新增路线产品
This commit is contained in:
+96
@@ -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));
|
||||
}
|
||||
}
|
||||
+11
-33
@@ -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
|
||||
|
||||
@@ -63,6 +63,15 @@ public class ExcelGrid
|
||||
* 按指定行数上限读取。邮编分区表会超过价卡默认的 2500 行。
|
||||
*/
|
||||
public static ExcelGrid read(Sheet sheet, int maxRows)
|
||||
{
|
||||
return read(sheet, maxRows, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取工作表,并在扫描、填值时回报行号。progress 为空时与 {@link #read(Sheet, int)} 相同。
|
||||
* phase 0 是统计列宽,phase 1 是读取单元格。
|
||||
*/
|
||||
public static ExcelGrid read(Sheet sheet, int maxRows, ReadProgress progress)
|
||||
{
|
||||
int limit = maxRows < 1 ? MAX_ROWS : maxRows;
|
||||
int lastRow = sheet == null || sheet.getLastRowNum() < 0 ? -1 : Math.min(sheet.getLastRowNum(), limit - 1);
|
||||
@@ -85,6 +94,7 @@ public class ExcelGrid
|
||||
cols = c + 1;
|
||||
}
|
||||
}
|
||||
reportRow(progress, 0, r, lastRow + 1);
|
||||
}
|
||||
}
|
||||
int rows = lastRow + 1;
|
||||
@@ -113,6 +123,7 @@ public class ExcelGrid
|
||||
{
|
||||
fill(row.getCell(c), text, number, linkSheet, r, c);
|
||||
}
|
||||
reportRow(progress, 1, r, rows);
|
||||
}
|
||||
for (CellRangeAddress region : sheet.getMergedRegions())
|
||||
{
|
||||
@@ -144,6 +155,27 @@ public class ExcelGrid
|
||||
return new ExcelGrid(text, number, linkSheet, colSpan, rowAnchor, rows, cols);
|
||||
}
|
||||
|
||||
/** 读取进度。大约每 20 行回报一次,避免大表回调过密。 */
|
||||
private static void reportRow(ReadProgress progress, int phase, int row, int total)
|
||||
{
|
||||
if (progress == null || total < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (row % 20 != 0 && row != total - 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
progress.onRow(phase, row, total);
|
||||
}
|
||||
|
||||
/** 工作表读取进度。phase 0 统计列宽,phase 1 读取单元格。 */
|
||||
@FunctionalInterface
|
||||
public interface ReadProgress
|
||||
{
|
||||
void onRow(int phase, int row, int total);
|
||||
}
|
||||
|
||||
public int rows()
|
||||
{
|
||||
return rows;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bms.masterdata.domain;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.bms.common.annotation.Excel;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -14,72 +15,136 @@ import lombok.Data;
|
||||
@Data
|
||||
public class LineProduct
|
||||
{
|
||||
/** 源主键 ID */
|
||||
/** 源主键 ID。列表、表单和导出都不展示。 */
|
||||
private String id;
|
||||
|
||||
/** 产品(线路)编码 */
|
||||
@Excel(name = "产品编码")
|
||||
private String lineCode;
|
||||
|
||||
/** 产品(线路)名称 */
|
||||
@Excel(name = "产品名称")
|
||||
private String lineName;
|
||||
|
||||
@Excel(name = "产品类型")
|
||||
private String productType;
|
||||
|
||||
@Excel(name = "线路类型")
|
||||
private String lineType;
|
||||
|
||||
/** 是否可收货(1是 0否) */
|
||||
@Excel(name = "是否可收货", readConverterExp = "1=是,0=否")
|
||||
private String blCargo;
|
||||
|
||||
@Excel(name = "货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@Excel(name = "仓储服务名称")
|
||||
private String productStorage;
|
||||
|
||||
@Excel(name = "头程服务名称")
|
||||
private String productFirst;
|
||||
|
||||
@Excel(name = "清关服务名称")
|
||||
private String productClearance;
|
||||
|
||||
@Excel(name = "仓储服务编码")
|
||||
private String productStorageCode;
|
||||
|
||||
@Excel(name = "头程服务编码")
|
||||
private String productFirstCode;
|
||||
|
||||
@Excel(name = "清关服务编码")
|
||||
private String productClearanceCode;
|
||||
|
||||
/** 是否启用(1启用 0停用) */
|
||||
@Excel(name = "是否启用", readConverterExp = "1=启用,0=停用")
|
||||
private String blOpen;
|
||||
|
||||
/** 是否分段(1是 0否) */
|
||||
@Excel(name = "是否分段", readConverterExp = "1=是,0=否")
|
||||
private String blSection;
|
||||
|
||||
@Excel(name = "派送商编码")
|
||||
private String dispCode;
|
||||
|
||||
@Excel(name = "派送商名称")
|
||||
private String dispName;
|
||||
|
||||
@Excel(name = "费用类别")
|
||||
private String feeClass;
|
||||
|
||||
@Excel(name = "发货人ID")
|
||||
private String shipperId;
|
||||
|
||||
/** 发货规则 */
|
||||
@Excel(name = "发货规则")
|
||||
private String shipmentRule;
|
||||
|
||||
/** 限重(kg) */
|
||||
@Excel(name = "限重(kg)")
|
||||
private BigDecimal weight;
|
||||
|
||||
@Excel(name = "货物类型")
|
||||
private String cargoType;
|
||||
|
||||
@Excel(name = "产品分类")
|
||||
private String productCategory;
|
||||
|
||||
/** 体积基数(抛货系数) */
|
||||
@Excel(name = "体积基数")
|
||||
private BigDecimal volumeBasis;
|
||||
|
||||
@Excel(name = "重量类型")
|
||||
private String weightType;
|
||||
|
||||
@Excel(name = "服务类型")
|
||||
private String serviceType;
|
||||
|
||||
@Excel(name = "发货类型")
|
||||
private String shipmentType;
|
||||
|
||||
@Excel(name = "税费金额")
|
||||
private BigDecimal taxAmount;
|
||||
|
||||
@Excel(name = "税费币种")
|
||||
private String taxCurrency;
|
||||
|
||||
@Excel(name = "电池类型")
|
||||
private String batteryType;
|
||||
|
||||
@Excel(name = "备注")
|
||||
private String remark;
|
||||
|
||||
/** 源创建人,页面不展示 */
|
||||
private String createName;
|
||||
|
||||
/** 源创建人工号,页面不展示 */
|
||||
private String createManCode;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@Excel(name = "创建时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createDate;
|
||||
|
||||
@Excel(name = "修改人")
|
||||
private String modifier;
|
||||
|
||||
@Excel(name = "修改人工号")
|
||||
private String modifierCode;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@Excel(name = "修改时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date modifyDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@Excel(name = "GMT创建时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date gmtCreate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@Excel(name = "GMT修改时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date gmtModified;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@Excel(name = "同步时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date syncTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.bms.masterdata.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.masterdata.domain.LineProduct;
|
||||
|
||||
/**
|
||||
* 路线产品 srv_line_product
|
||||
*/
|
||||
public interface LineProductMapper
|
||||
{
|
||||
List<LineProduct> selectLineProductList(LineProduct query);
|
||||
|
||||
LineProduct selectLineProductById(String id);
|
||||
|
||||
LineProduct selectLineProductByCode(String lineCode);
|
||||
|
||||
int countProductChannelByProductId(String productId);
|
||||
|
||||
int insertLineProduct(LineProduct product);
|
||||
|
||||
int updateLineProduct(LineProduct product);
|
||||
|
||||
int updateLineProductStatus(LineProduct product);
|
||||
|
||||
int deleteLineProductByIds(String[] ids);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.bms.masterdata.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.masterdata.domain.LineProduct;
|
||||
|
||||
/**
|
||||
* 路线产品
|
||||
*/
|
||||
public interface ILineProductService
|
||||
{
|
||||
List<LineProduct> selectLineProductList(LineProduct query);
|
||||
|
||||
LineProduct selectLineProductById(String id);
|
||||
|
||||
int insertLineProduct(LineProduct product);
|
||||
|
||||
int updateLineProduct(LineProduct product);
|
||||
|
||||
int updateLineProductStatus(LineProduct product);
|
||||
|
||||
int deleteLineProductByIds(String[] ids);
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package com.bms.masterdata.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.masterdata.domain.LineProduct;
|
||||
import com.bms.masterdata.mapper.LineProductMapper;
|
||||
import com.bms.masterdata.service.ILineProductService;
|
||||
|
||||
/**
|
||||
* 路线产品
|
||||
*/
|
||||
@Service
|
||||
public class LineProductServiceImpl implements ILineProductService
|
||||
{
|
||||
@Autowired
|
||||
private LineProductMapper lineProductMapper;
|
||||
|
||||
@Override
|
||||
public List<LineProduct> selectLineProductList(LineProduct query)
|
||||
{
|
||||
return lineProductMapper.selectLineProductList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LineProduct selectLineProductById(String id)
|
||||
{
|
||||
return lineProductMapper.selectLineProductById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertLineProduct(LineProduct product)
|
||||
{
|
||||
prepare(product, true);
|
||||
product.setId(IdUtils.fastSimpleUUID());
|
||||
assertCodeUnique(product);
|
||||
return lineProductMapper.insertLineProduct(product);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateLineProduct(LineProduct product)
|
||||
{
|
||||
if (product == null || StringUtils.isEmpty(product.getId()))
|
||||
{
|
||||
throw new ServiceException("路线产品不存在");
|
||||
}
|
||||
LineProduct existing = lineProductMapper.selectLineProductById(product.getId());
|
||||
if (existing == null)
|
||||
{
|
||||
throw new ServiceException("路线产品不存在或已被删除");
|
||||
}
|
||||
prepare(product, false);
|
||||
assertCodeUnique(product);
|
||||
return lineProductMapper.updateLineProduct(product);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateLineProductStatus(LineProduct product)
|
||||
{
|
||||
if (product == null || StringUtils.isEmpty(product.getId()))
|
||||
{
|
||||
throw new ServiceException("路线产品不存在");
|
||||
}
|
||||
if (!"0".equals(product.getBlOpen()) && !"1".equals(product.getBlOpen()))
|
||||
{
|
||||
throw new ServiceException("启用状态只能是启用或停用");
|
||||
}
|
||||
LineProduct existing = lineProductMapper.selectLineProductById(product.getId());
|
||||
if (existing == null)
|
||||
{
|
||||
throw new ServiceException("路线产品不存在或已被删除");
|
||||
}
|
||||
return lineProductMapper.updateLineProductStatus(product);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteLineProductByIds(String[] ids)
|
||||
{
|
||||
if (ids == null || ids.length == 0)
|
||||
{
|
||||
throw new ServiceException("请选择要删除的路线产品");
|
||||
}
|
||||
for (String id : ids)
|
||||
{
|
||||
if (StringUtils.isEmpty(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
LineProduct existing = lineProductMapper.selectLineProductById(id);
|
||||
if (existing == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (lineProductMapper.countProductChannelByProductId(id) > 0)
|
||||
{
|
||||
throw new ServiceException("路线产品「" + existing.getLineName() + "」已被渠道关系引用,不能删除,请改为停用");
|
||||
}
|
||||
}
|
||||
return lineProductMapper.deleteLineProductByIds(ids);
|
||||
}
|
||||
|
||||
private void prepare(LineProduct product, boolean creating)
|
||||
{
|
||||
if (product == null)
|
||||
{
|
||||
throw new ServiceException("路线产品不能为空");
|
||||
}
|
||||
product.setLineCode(StringUtils.trim(product.getLineCode()));
|
||||
product.setLineName(StringUtils.trim(product.getLineName()));
|
||||
product.setProductType(limit(product.getProductType(), 20, "产品类型"));
|
||||
product.setLineType(limit(product.getLineType(), 50, "线路类型"));
|
||||
product.setCargoName(limit(product.getCargoName(), 100, "货物名称"));
|
||||
product.setProductStorage(trimToNull(product.getProductStorage()));
|
||||
product.setProductFirst(trimToNull(product.getProductFirst()));
|
||||
product.setProductClearance(trimToNull(product.getProductClearance()));
|
||||
product.setProductStorageCode(limit(product.getProductStorageCode(), 1000, "仓储服务编码"));
|
||||
product.setProductFirstCode(limit(product.getProductFirstCode(), 1000, "头程服务编码"));
|
||||
product.setProductClearanceCode(limit(product.getProductClearanceCode(), 1000, "清关服务编码"));
|
||||
product.setDispCode(limit(product.getDispCode(), 60, "派送商编码"));
|
||||
product.setDispName(limit(product.getDispName(), 100, "派送商名称"));
|
||||
product.setFeeClass(limit(product.getFeeClass(), 100, "费用类别"));
|
||||
product.setShipperId(limit(product.getShipperId(), 64, "发货人ID"));
|
||||
product.setShipmentRule(flagOrNull(product.getShipmentRule(), "发货规则"));
|
||||
product.setCargoType(limit(product.getCargoType(), 64, "货物类型"));
|
||||
product.setProductCategory(limit(product.getProductCategory(), 100, "产品分类"));
|
||||
product.setWeightType(flagOrNull(product.getWeightType(), "重量类型"));
|
||||
product.setServiceType(flagOrNull(product.getServiceType(), "服务类型"));
|
||||
product.setShipmentType(limit(product.getShipmentType(), 32, "发货类型"));
|
||||
product.setTaxCurrency(limit(product.getTaxCurrency(), 16, "税费币种"));
|
||||
product.setBatteryType(limit(product.getBatteryType(), 64, "电池类型"));
|
||||
product.setRemark(limit(product.getRemark(), 500, "备注"));
|
||||
product.setBlCargo(yesNo(product.getBlCargo(), "是否可收货"));
|
||||
product.setBlSection(yesNo(product.getBlSection(), "是否分段"));
|
||||
if (StringUtils.isEmpty(product.getLineCode()))
|
||||
{
|
||||
throw new ServiceException("产品编码不能为空");
|
||||
}
|
||||
if (product.getLineCode().length() > 64)
|
||||
{
|
||||
throw new ServiceException("产品编码不能超过64个字符");
|
||||
}
|
||||
if (StringUtils.isEmpty(product.getLineName()))
|
||||
{
|
||||
throw new ServiceException("产品名称不能为空");
|
||||
}
|
||||
if (product.getLineName().length() > 200)
|
||||
{
|
||||
throw new ServiceException("产品名称不能超过200个字符");
|
||||
}
|
||||
if (StringUtils.isEmpty(product.getBlOpen()))
|
||||
{
|
||||
if (creating)
|
||||
{
|
||||
product.setBlOpen("1");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ServiceException("请选择启用状态");
|
||||
}
|
||||
}
|
||||
if (!"0".equals(product.getBlOpen()) && !"1".equals(product.getBlOpen()))
|
||||
{
|
||||
throw new ServiceException("启用状态只能是启用或停用");
|
||||
}
|
||||
if (product.getWeight() != null && product.getWeight().compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
throw new ServiceException("限重不能为负数");
|
||||
}
|
||||
if (product.getVolumeBasis() != null && product.getVolumeBasis().compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
throw new ServiceException("体积基数不能为负数");
|
||||
}
|
||||
if (product.getTaxAmount() != null && product.getTaxAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
throw new ServiceException("税费金额不能为负数");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertCodeUnique(LineProduct product)
|
||||
{
|
||||
LineProduct same = lineProductMapper.selectLineProductByCode(product.getLineCode());
|
||||
if (same != null && !StringUtils.equals(same.getId(), product.getId()))
|
||||
{
|
||||
throw new ServiceException("产品编码「" + product.getLineCode() + "」已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private String yesNo(String value, String label)
|
||||
{
|
||||
String text = trimToNull(value);
|
||||
if (text != null && !"0".equals(text) && !"1".equals(text))
|
||||
{
|
||||
throw new ServiceException(label + "只能是是或否");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/** 单字符标记:空则清空,有值时只能是一个字符。 */
|
||||
private String flagOrNull(String value, String label)
|
||||
{
|
||||
String text = trimToNull(value);
|
||||
if (text != null && text.length() > 1)
|
||||
{
|
||||
throw new ServiceException(label + "只能填写一个字符");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private String limit(String value, int max, String label)
|
||||
{
|
||||
String text = trimToNull(value);
|
||||
if (text != null && text.length() > max)
|
||||
{
|
||||
throw new ServiceException(label + "不能超过" + max + "个字符");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private String trimToNull(String value)
|
||||
{
|
||||
String text = StringUtils.trim(value);
|
||||
return StringUtils.isEmpty(text) ? null : text;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 一个应付产品上的附加费开关。多个产品各自一组,互不影响。
|
||||
*/
|
||||
@Data
|
||||
public class PayFeeRuleGroup
|
||||
{
|
||||
private Long productId;
|
||||
|
||||
private String productName;
|
||||
|
||||
private List<PayProductFeeRule> rules = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
/**
|
||||
* 应付价卡解析进度。给导入弹框轮询,不入库。
|
||||
*/
|
||||
public class PayParseProgress
|
||||
{
|
||||
public static final String PENDING = "PENDING";
|
||||
|
||||
public static final String RUNNING = "RUNNING";
|
||||
|
||||
public static final String SUCCESS = "SUCCESS";
|
||||
|
||||
public static final String FAIL = "FAIL";
|
||||
|
||||
private int percent;
|
||||
|
||||
private String message;
|
||||
|
||||
private String status;
|
||||
|
||||
public PayParseProgress()
|
||||
{
|
||||
}
|
||||
|
||||
public PayParseProgress(int percent, String message, String status)
|
||||
{
|
||||
this.percent = percent;
|
||||
this.message = message;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getPercent()
|
||||
{
|
||||
return percent;
|
||||
}
|
||||
|
||||
public void setPercent(int percent)
|
||||
{
|
||||
this.percent = percent;
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status)
|
||||
{
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 应付产品附加费开关。挂在根产品上,价格仍在价卡版本的附加费行里。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PayProductFeeRule extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long ruleId;
|
||||
|
||||
/** 根产品编号 */
|
||||
private Long productId;
|
||||
|
||||
/** 附加费名称,用来到价卡版本里取价 */
|
||||
private String feeName;
|
||||
|
||||
/** 1 参与计算,0 不参与 */
|
||||
private String enabled;
|
||||
|
||||
/** SIZE 尺寸,VOLUME 体积,WEIGHT 重量,POSTCODE 邮编,ALWAYS 每票,RETURN 退件 */
|
||||
private String triggerType;
|
||||
|
||||
private BigDecimal sideMaxCm;
|
||||
|
||||
private BigDecimal sumMaxCm;
|
||||
|
||||
private BigDecimal volumeMaxCm3;
|
||||
|
||||
private BigDecimal minLengthCm;
|
||||
|
||||
private BigDecimal minWidthCm;
|
||||
|
||||
private BigDecimal minHeightCm;
|
||||
|
||||
private BigDecimal weightMaxKg;
|
||||
|
||||
/** ANY 任一条超出就收,ALL 写了的条件都超出才收 */
|
||||
private String joinMode;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class PaySurcharge
|
||||
/** 固定收取、产生时收取、旺季期间 */
|
||||
private String chargeTiming;
|
||||
|
||||
/** ALWAYS、POSTCODE、WEIGHT、SIZE、RETURN、MANUAL */
|
||||
/** ALWAYS、POSTCODE、WEIGHT、SIZE、RETURN、EXTRA_KG、MANUAL */
|
||||
private String calcRule;
|
||||
|
||||
/** 邮编列表、重量阈值或尺寸表达式 */
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.bms.price.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.price.domain.PayProductFeeRule;
|
||||
|
||||
/**
|
||||
* 应付产品附加费开关
|
||||
*/
|
||||
public interface PayProductFeeRuleMapper
|
||||
{
|
||||
List<PayProductFeeRule> selectByProductId(Long productId);
|
||||
|
||||
int deleteByProductId(Long productId);
|
||||
|
||||
int deleteByProductIds(@Param("productIds") List<Long> productIds);
|
||||
|
||||
int insertBatch(@Param("list") List<PayProductFeeRule> list);
|
||||
}
|
||||
@@ -68,8 +68,14 @@ public final class PayCostCalculator
|
||||
PayWeightBand band = matchBand(bands, weight);
|
||||
if (band == null)
|
||||
{
|
||||
result.setMessage(outOfRange(product, bands, weight));
|
||||
return result;
|
||||
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);
|
||||
@@ -81,7 +87,8 @@ public final class PayCostCalculator
|
||||
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, result);
|
||||
addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
|
||||
band.getSectionName(), result);
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (PayCostLine fee : result.getFees())
|
||||
{
|
||||
@@ -111,7 +118,7 @@ public final class PayCostCalculator
|
||||
|
||||
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,
|
||||
PayOrderCostResult result)
|
||||
String freightSection, PayOrderCostResult result)
|
||||
{
|
||||
if (product.getSurcharges() == null)
|
||||
{
|
||||
@@ -171,6 +178,13 @@ public final class PayCostCalculator
|
||||
addFlat(result.getFees(), fee, currency, "订单已退件");
|
||||
}
|
||||
}
|
||||
else if (PaySurchargeRules.EXTRA_KG.equals(rule))
|
||||
{
|
||||
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()));
|
||||
@@ -214,6 +228,97 @@ public final class PayCostCalculator
|
||||
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)
|
||||
{
|
||||
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 void addFlat(List<PayCostLine> fees, PaySurcharge fee, String currency, String remark)
|
||||
{
|
||||
PayCostLine line = matchedFee(fee, null, currency, remark);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.bms.price.pay;
|
||||
|
||||
/**
|
||||
* 应付价卡解析进度。只用于界面展示,不参与识别。
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface PayParseProgressListener
|
||||
{
|
||||
void onProgress(int percent, String message);
|
||||
}
|
||||
@@ -26,6 +26,9 @@ public final class PaySurchargeRules
|
||||
|
||||
public static final String MANUAL = "MANUAL";
|
||||
|
||||
/** 超过重量段上限后,按超出的公斤数乘单价 */
|
||||
public static final String EXTRA_KG = "EXTRA_KG";
|
||||
|
||||
private static final Pattern RANGE = Pattern.compile("([A-Z]{1,2})(\\d{1,2})\\s*-\\s*(\\d{1,2})");
|
||||
|
||||
private static final Pattern DISTRICT = Pattern.compile("([A-Z]{1,2})(\\d{1,2})");
|
||||
|
||||
@@ -112,6 +112,15 @@ public class PayWorkbookParser
|
||||
* 单元格里用分号隔开的国家、分区或费用名称,拆成多条。
|
||||
*/
|
||||
public PayPreview parseMatched(byte[] bytes, String filename, String selected, Collection<String> dictValues)
|
||||
{
|
||||
return parseMatched(bytes, filename, selected, dictValues, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 {@link #parseMatched(byte[], String, String, Collection)} 相同。listener 只接收进度,不改变识别结果。
|
||||
*/
|
||||
public PayPreview parseMatched(byte[] bytes, String filename, String selected, Collection<String> dictValues,
|
||||
PayParseProgressListener listener)
|
||||
{
|
||||
if (bytes == null || bytes.length == 0)
|
||||
{
|
||||
@@ -128,6 +137,7 @@ public class PayWorkbookParser
|
||||
}
|
||||
String safeName = filename == null ? "价卡.xlsx" : filename;
|
||||
String wanted = norm(selected);
|
||||
report(listener, 8, "正在打开工作簿");
|
||||
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes)))
|
||||
{
|
||||
if (workbook.getNumberOfSheets() == 0)
|
||||
@@ -141,29 +151,43 @@ public class PayWorkbookParser
|
||||
preview.setSupplierName(meta.supplier);
|
||||
preview.setEffectiveDate(PayTexts.toDate(meta.effective));
|
||||
List<String> skipped = new ArrayList<>();
|
||||
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||
int sheetCount = workbook.getNumberOfSheets();
|
||||
for (int i = 0; i < sheetCount; i++)
|
||||
{
|
||||
Sheet sheet = workbook.getSheetAt(i);
|
||||
String sheetName = sheet.getSheetName() == null ? "" : sheet.getSheetName().trim();
|
||||
int start = 12 + i * 78 / sheetCount;
|
||||
int end = 12 + (i + 1) * 78 / sheetCount;
|
||||
String dictValue = dict.get(norm(sheetName));
|
||||
boolean holdForFallback = sheetCount == 1 && !wanted.isEmpty();
|
||||
if (dictValue == null)
|
||||
{
|
||||
skipped.add(sheetName);
|
||||
if (!holdForFallback)
|
||||
{
|
||||
report(listener, end, "已跳过「" + sheetName + "」");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!wanted.isEmpty() && !wanted.equals(norm(dictValue)) && !wanted.equals(norm(sheetName)))
|
||||
{
|
||||
skipped.add(sheetName);
|
||||
if (!holdForFallback)
|
||||
{
|
||||
report(listener, end, "已跳过「" + sheetName + "」");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
preview.getProducts().add(readMatchedSheet(sheet, sheetName, dictValue, meta, preview));
|
||||
report(listener, start, "正在解析「" + sheetName + "」");
|
||||
preview.getProducts().add(readMatchedSheet(sheet, sheetName, dictValue, meta, preview, listener, start, end));
|
||||
}
|
||||
if (preview.getProducts().isEmpty() && !wanted.isEmpty() && workbook.getNumberOfSheets() == 1)
|
||||
if (preview.getProducts().isEmpty() && !wanted.isEmpty() && sheetCount == 1)
|
||||
{
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
String sheetName = sheet.getSheetName() == null ? "" : sheet.getSheetName().trim();
|
||||
String dictValue = dict.containsValue(selected.trim()) ? selected.trim() : labelOf(dict, wanted);
|
||||
preview.getProducts().add(readMatchedSheet(sheet, sheetName, dictValue, meta, preview));
|
||||
report(listener, 12, "正在解析「" + sheetName + "」");
|
||||
preview.getProducts().add(readMatchedSheet(sheet, sheetName, dictValue, meta, preview, listener, 12, 90));
|
||||
skipped.clear();
|
||||
}
|
||||
if (preview.getProducts().isEmpty())
|
||||
@@ -172,8 +196,10 @@ public class PayWorkbookParser
|
||||
? "没有工作表能对上应付产品字典"
|
||||
: "文件里没有「" + selected.trim() + "」这张工作表");
|
||||
}
|
||||
report(listener, 92, "正在整理附加费");
|
||||
attachWorkbookRates(workbook, preview.getProducts());
|
||||
preview.setSkippedSheets(skipped);
|
||||
report(listener, 96, "工作表解析完成");
|
||||
return preview;
|
||||
}
|
||||
catch (ServiceException ex)
|
||||
@@ -186,7 +212,24 @@ public class PayWorkbookParser
|
||||
}
|
||||
}
|
||||
|
||||
private PayProduct readMatchedSheet(Sheet sheet, String sheetName, String dictValue, FileMeta meta, PayPreview preview)
|
||||
private static void report(PayParseProgressListener listener, int percent, String message)
|
||||
{
|
||||
if (listener == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
listener.onProgress(percent, message);
|
||||
}
|
||||
catch (RuntimeException ignored)
|
||||
{
|
||||
// 进度写失败不影响价卡识别
|
||||
}
|
||||
}
|
||||
|
||||
private PayProduct readMatchedSheet(Sheet sheet, String sheetName, String dictValue, FileMeta meta, PayPreview preview,
|
||||
PayParseProgressListener listener, int start, int end)
|
||||
{
|
||||
PayProduct product = new PayProduct();
|
||||
product.setProductName(dictValue);
|
||||
@@ -199,11 +242,30 @@ public class PayWorkbookParser
|
||||
product.setWeightBands(new ArrayList<>());
|
||||
product.setSurcharges(new ArrayList<>());
|
||||
product.setExtraLines(new ArrayList<>());
|
||||
fillSheet(ExcelGrid.read(sheet), product);
|
||||
fillSheet(readGrid(sheet, listener, start, end, sheetName), product);
|
||||
expandSeparated(product);
|
||||
report(listener, end, "已解析「" + sheetName + "」");
|
||||
return product;
|
||||
}
|
||||
|
||||
/** 读取这一张表。前半段是统计列宽,后半段是读取单元格。 */
|
||||
private ExcelGrid readGrid(Sheet sheet, PayParseProgressListener listener, int start, int end, String sheetName)
|
||||
{
|
||||
int span = Math.max(1, end - start);
|
||||
int scanSpan = Math.max(1, span * 2 / 5);
|
||||
return ExcelGrid.read(sheet, ExcelGrid.MAX_ROWS, (phase, row, total) -> {
|
||||
if (total < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int done = Math.min(row + 1, total);
|
||||
int percent = phase == 0
|
||||
? start + scanSpan * done / total
|
||||
: start + scanSpan + (span - scanSpan) * done / total;
|
||||
report(listener, Math.min(end, percent), "正在解析「" + sheetName + "」");
|
||||
});
|
||||
}
|
||||
|
||||
/** 分号拆开的是多条数据,不是一句话。句子太长时保持原样。 */
|
||||
private void expandSeparated(PayProduct product)
|
||||
{
|
||||
@@ -1848,7 +1910,8 @@ public class PayWorkbookParser
|
||||
|
||||
/**
|
||||
* 岛屿价卡一张表有两级表头:上面是群岛,下面是具体岛屿。
|
||||
* Up to 1 kg 是首重,Additional Kg 是续重,Cubic Capacity 是体积重系数,不是运费。
|
||||
* Up to 1 kg 是首重,记入重量段。Additional Kg 是超出首重后的每公斤加收,记入附加费。
|
||||
* Cubic Capacity 是体积重系数,不是运费。
|
||||
*/
|
||||
private int readInsularRates(ExcelGrid grid, int header, PayProduct product)
|
||||
{
|
||||
@@ -1869,6 +1932,7 @@ public class PayWorkbookParser
|
||||
zoneCols.add(col);
|
||||
}
|
||||
}
|
||||
BigDecimal firstLimit = null;
|
||||
int row = header + 1;
|
||||
while (row < grid.rows() && !grid.blankRow(row))
|
||||
{
|
||||
@@ -1887,6 +1951,10 @@ public class PayWorkbookParser
|
||||
row++;
|
||||
continue;
|
||||
}
|
||||
if (firstKg)
|
||||
{
|
||||
firstLimit = firstKgLimit(label);
|
||||
}
|
||||
for (int col : zoneCols)
|
||||
{
|
||||
String child = grid.text(header, col).replace('\n', ' ').trim();
|
||||
@@ -1902,23 +1970,20 @@ public class PayWorkbookParser
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (extraKg)
|
||||
{
|
||||
addExtraKg(product, section, zone, label, price, firstLimit == null ? BigDecimal.ONE : firstLimit);
|
||||
continue;
|
||||
}
|
||||
PayWeightBand band = new PayWeightBand();
|
||||
band.setSectionName(section);
|
||||
band.setZoneName(zone);
|
||||
band.setWeightLabel(label);
|
||||
band.setPrice(price);
|
||||
if (extraKg)
|
||||
{
|
||||
band.setChargeName("每公斤");
|
||||
band.setPriceUnit("EUR/公斤");
|
||||
}
|
||||
else
|
||||
{
|
||||
band.setWeightMin(BigDecimal.ZERO);
|
||||
band.setWeightMax(firstKgLimit(label));
|
||||
band.setChargeName("每件");
|
||||
band.setPriceUnit("EUR/件");
|
||||
}
|
||||
band.setWeightMin(BigDecimal.ZERO);
|
||||
band.setWeightMax(firstKgLimit(label));
|
||||
band.setChargeName("每件");
|
||||
band.setPriceUnit("EUR/件");
|
||||
band.setSortNo(product.getWeightBands().size() + 1);
|
||||
product.getWeightBands().add(band);
|
||||
}
|
||||
@@ -1928,7 +1993,8 @@ public class PayWorkbookParser
|
||||
}
|
||||
|
||||
/**
|
||||
* 西班牙 CTT:上面一行是大陆或群岛,下一行是密度和岛屿。1kg 到 5kg 是该重量的票价,Kg.ad. 是超出后的续重。
|
||||
* 西班牙 CTT:上面一行是大陆或群岛,下一行是密度和岛屿。
|
||||
* 1kg 到 5kg 是该重量的票价,记入重量段。Kg.ad. 是超出最高档后的每公斤加收,记入附加费。
|
||||
*/
|
||||
private int readKgStepTable(ExcelGrid grid, int header, PayProduct product)
|
||||
{
|
||||
@@ -1969,25 +2035,25 @@ public class PayWorkbookParser
|
||||
}
|
||||
String child = grid.text(header, col).replace('\n', ' ').trim();
|
||||
String parent = groupLabel(grid, groupRow, col, service);
|
||||
PayWeightBand band = new PayWeightBand();
|
||||
band.setSectionName(service);
|
||||
band.setZoneName(parent.isBlank() ? child : parent + " / " + child);
|
||||
band.setWeightLabel(label);
|
||||
band.setPrice(price);
|
||||
String zone = parent.isBlank() ? child : parent + " / " + child;
|
||||
if (extra)
|
||||
{
|
||||
band.setChargeName("每公斤");
|
||||
band.setPriceUnit("EUR/公斤");
|
||||
addExtraKg(product, service, zone, label, price, previousMax);
|
||||
}
|
||||
else
|
||||
{
|
||||
PayWeightBand band = new PayWeightBand();
|
||||
band.setSectionName(service);
|
||||
band.setZoneName(zone);
|
||||
band.setWeightLabel(label);
|
||||
band.setPrice(price);
|
||||
band.setWeightMin(minKg);
|
||||
band.setWeightMax(maxKg);
|
||||
band.setChargeName("每件");
|
||||
band.setPriceUnit("EUR/件");
|
||||
band.setSortNo(product.getWeightBands().size() + 1);
|
||||
product.getWeightBands().add(band);
|
||||
}
|
||||
band.setSortNo(product.getWeightBands().size() + 1);
|
||||
product.getWeightBands().add(band);
|
||||
}
|
||||
if (!extra)
|
||||
{
|
||||
@@ -1998,6 +2064,26 @@ public class PayWorkbookParser
|
||||
return row;
|
||||
}
|
||||
|
||||
/** Kg.ad. 按地区各记一条每公斤续重,不进入重量段。 */
|
||||
private static void addExtraKg(PayProduct product, String section, String zone, String label, BigDecimal price, BigDecimal threshold)
|
||||
{
|
||||
BigDecimal from = threshold == null ? BigDecimal.ZERO : threshold;
|
||||
PaySurcharge fee = new PaySurcharge();
|
||||
fee.setSectionName(section);
|
||||
fee.setFeeName("每公斤续重");
|
||||
fee.setZoneName(zone);
|
||||
fee.setChargeText(label + " " + price.stripTrailingZeros().toPlainString() + " EUR/KG");
|
||||
fee.setAmount(price);
|
||||
fee.setCurrency("EUR");
|
||||
fee.setPriceUnit("EUR/KG");
|
||||
fee.setFeeDesc("超过" + from.stripTrailingZeros().toPlainString() + "kg后,按超出的重量每公斤加收");
|
||||
fee.setChargeTiming("产生时收取");
|
||||
fee.setCalcRule(PaySurchargeRules.EXTRA_KG);
|
||||
fee.setCalcParam(from.stripTrailingZeros().toPlainString());
|
||||
fee.setSortNo(product.getSurcharges().size() + 1);
|
||||
product.getSurcharges().add(fee);
|
||||
}
|
||||
|
||||
private static boolean isKgStepHeader(ExcelGrid grid, int row)
|
||||
{
|
||||
int names = 0;
|
||||
@@ -2260,6 +2346,16 @@ public class PayWorkbookParser
|
||||
itemFee = true;
|
||||
}
|
||||
}
|
||||
if (product.getSurcharges() != null)
|
||||
{
|
||||
for (PaySurcharge fee : product.getSurcharges())
|
||||
{
|
||||
if (fee != null && PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()))
|
||||
{
|
||||
extraKg = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (extraKg)
|
||||
{
|
||||
return "续重计费";
|
||||
@@ -2297,6 +2393,24 @@ public class PayWorkbookParser
|
||||
high = band.getPrice();
|
||||
}
|
||||
}
|
||||
if (product.getSurcharges() != null)
|
||||
{
|
||||
for (PaySurcharge fee : product.getSurcharges())
|
||||
{
|
||||
if (fee == null || fee.getAmount() == null || !PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (low == null || fee.getAmount().compareTo(low) < 0)
|
||||
{
|
||||
low = fee.getAmount();
|
||||
}
|
||||
if (high == null || fee.getAmount().compareTo(high) > 0)
|
||||
{
|
||||
high = fee.getAmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (low == null)
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.bms.price.service;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Date;
|
||||
import com.bms.price.domain.PayFeeRuleGroup;
|
||||
import com.bms.price.domain.PayParseProgress;
|
||||
import com.bms.price.domain.PayPreview;
|
||||
import com.bms.price.domain.PayPriceCompare;
|
||||
import com.bms.price.domain.PayProduct;
|
||||
@@ -21,6 +21,14 @@ public interface IPayProductService
|
||||
*/
|
||||
PayPreview preview(byte[] bytes, String filename, String dictValue);
|
||||
|
||||
/**
|
||||
* 与 {@link #preview(byte[], String, String)} 相同。progressId 有值时写入解析进度,供页面轮询。
|
||||
*/
|
||||
PayPreview preview(byte[] bytes, String filename, String dictValue, String progressId);
|
||||
|
||||
/** 读取一次导入的解析进度。没有对应任务时返回等待中。 */
|
||||
PayParseProgress parseProgress(String progressId);
|
||||
|
||||
/** 把预览里勾选的产品写入数据库。同名字典产品再次导入时另存一版,旧价格留在有效期表里 */
|
||||
String save(PaySaveCommand command);
|
||||
|
||||
@@ -75,10 +83,4 @@ public interface IPayProductService
|
||||
void writeSource(Long productId, OutputStream output);
|
||||
|
||||
int deletePayProducts(Long[] productIds);
|
||||
|
||||
/** 按根产品编号带出附加费开关。没有配置过的产品返回空列表 */
|
||||
List<PayFeeRuleGroup> listFeeRules(List<Long> productIds);
|
||||
|
||||
/** 按产品整组替换附加费开关。每个产品一份,互不影响 */
|
||||
void saveFeeRules(List<PayFeeRuleGroup> products);
|
||||
}
|
||||
|
||||
+112
-191
@@ -3,13 +3,11 @@ package com.bms.price.service.impl;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -28,8 +26,7 @@ import com.bms.common.utils.file.OssStorage;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.price.domain.ChannelProductMap;
|
||||
import com.bms.price.domain.PayExtraLine;
|
||||
import com.bms.price.domain.PayFeeRuleGroup;
|
||||
import com.bms.price.domain.PayProductFeeRule;
|
||||
import com.bms.price.domain.PayParseProgress;
|
||||
import com.bms.price.domain.PayPreview;
|
||||
import com.bms.price.domain.PayPriceCompare;
|
||||
import com.bms.price.domain.PayPriceDiff;
|
||||
@@ -39,9 +36,9 @@ import com.bms.price.domain.PaySaveCommand;
|
||||
import com.bms.price.domain.PaySurcharge;
|
||||
import com.bms.price.domain.PayWeightBand;
|
||||
import com.bms.price.mapper.ChannelProductMapMapper;
|
||||
import com.bms.price.mapper.PayProductFeeRuleMapper;
|
||||
import com.bms.price.mapper.PayProductMapper;
|
||||
import com.bms.price.mapper.PayProductTermMapper;
|
||||
import com.bms.price.pay.PayParseProgressListener;
|
||||
import com.bms.price.pay.PayWorkbookParser;
|
||||
import com.bms.price.service.IPayProductService;
|
||||
import com.bms.system.service.ISysDictTypeService;
|
||||
@@ -69,11 +66,6 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
@Autowired
|
||||
private PayProductTermMapper payProductTermMapper;
|
||||
|
||||
@Autowired
|
||||
private PayProductFeeRuleMapper payProductFeeRuleMapper;
|
||||
|
||||
private static final Set<String> FEE_TRIGGERS = Set.of("SIZE", "VOLUME", "WEIGHT", "POSTCODE", "ALWAYS", "RETURN");
|
||||
|
||||
@Autowired
|
||||
private ISysDictTypeService dictTypeService;
|
||||
|
||||
@@ -85,23 +77,110 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
/** 预览时已经传到 OSS 的文件。保存不再只靠前端把地址传回来。 */
|
||||
private final Map<String, UploadedSource> uploadedSources = new ConcurrentHashMap<>();
|
||||
|
||||
/** 导入弹框轮询的解析进度。只留在本机内存,过期后丢掉。 */
|
||||
private final Map<String, ParseSlot> parseProgress = new ConcurrentHashMap<>();
|
||||
|
||||
private static final long PARSE_PROGRESS_KEEP_MS = 15L * 60 * 1000;
|
||||
|
||||
private static final long SOURCE_REMEMBER_MS = 6L * 60 * 60 * 1000;
|
||||
|
||||
@Override
|
||||
public PayPreview preview(byte[] bytes, String filename, String dictValue)
|
||||
{
|
||||
PayPreview preview = parser.parseMatched(bytes, filename, dictValue, loadDictValues());
|
||||
String stored = storeSource(bytes, filename);
|
||||
rememberSource(preview.getFileMd5(), stored);
|
||||
preview.setFilePath(stored);
|
||||
for (PayProduct product : preview.getProducts())
|
||||
return preview(bytes, filename, dictValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayPreview preview(byte[] bytes, String filename, String dictValue, String progressId)
|
||||
{
|
||||
String progressKey = progressKey(progressId);
|
||||
PayParseProgressListener listener = progressKey == null ? null : (percent, message) ->
|
||||
writeProgress(progressKey, percent, message, PayParseProgress.RUNNING);
|
||||
if (progressKey != null)
|
||||
{
|
||||
product.setFilePath(stored);
|
||||
Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue());
|
||||
product.setDuplicated(existsId != null);
|
||||
product.setExistsId(existsId);
|
||||
writeProgress(progressKey, 2, "正在准备解析", PayParseProgress.RUNNING);
|
||||
}
|
||||
return preview;
|
||||
try
|
||||
{
|
||||
PayPreview preview = parser.parseMatched(bytes, filename, dictValue, loadDictValues(), listener);
|
||||
writeProgress(progressKey, 98, "正在核对已有产品", PayParseProgress.RUNNING);
|
||||
String stored = storeSource(bytes, filename);
|
||||
rememberSource(preview.getFileMd5(), stored);
|
||||
preview.setFilePath(stored);
|
||||
for (PayProduct product : preview.getProducts())
|
||||
{
|
||||
product.setFilePath(stored);
|
||||
Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue());
|
||||
product.setDuplicated(existsId != null);
|
||||
product.setExistsId(existsId);
|
||||
}
|
||||
writeProgress(progressKey, 100, "解析完成", PayParseProgress.SUCCESS);
|
||||
return preview;
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
String message = ex.getMessage() == null || ex.getMessage().isBlank() ? "价卡解析失败" : ex.getMessage();
|
||||
writeProgress(progressKey, 100, message, PayParseProgress.FAIL);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayParseProgress parseProgress(String progressId)
|
||||
{
|
||||
String key = progressKey(progressId);
|
||||
if (key == null)
|
||||
{
|
||||
return new PayParseProgress(0, "等待解析", PayParseProgress.PENDING);
|
||||
}
|
||||
ParseSlot slot = parseProgress.get(key);
|
||||
if (slot == null || System.currentTimeMillis() - slot.at > PARSE_PROGRESS_KEEP_MS)
|
||||
{
|
||||
return new PayParseProgress(0, "等待解析", PayParseProgress.PENDING);
|
||||
}
|
||||
return slot.progress;
|
||||
}
|
||||
|
||||
private void writeProgress(String key, int percent, String message, String status)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int safePercent = Math.max(0, Math.min(100, percent));
|
||||
ParseSlot current = parseProgress.get(key);
|
||||
if (current != null && safePercent < current.progress.getPercent()
|
||||
&& !PayParseProgress.FAIL.equals(status))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (current != null && safePercent == current.progress.getPercent()
|
||||
&& message.equals(current.progress.getMessage())
|
||||
&& status.equals(current.progress.getStatus()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
parseProgress.put(key, new ParseSlot(new PayParseProgress(safePercent, message, status), System.currentTimeMillis()));
|
||||
if (parseProgress.size() > 200)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
parseProgress.entrySet().removeIf(entry -> now - entry.getValue().at > PARSE_PROGRESS_KEEP_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/** 只接受页面生成的短编号,避免把任意字符串当成缓存键。 */
|
||||
private static String progressKey(String progressId)
|
||||
{
|
||||
if (progressId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String key = progressId.trim();
|
||||
if (key.length() < 8 || key.length() > 64 || !key.matches("[A-Za-z0-9-]+"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -680,7 +759,6 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return 0;
|
||||
}
|
||||
payProductTermMapper.deleteByProductIds(ids);
|
||||
payProductFeeRuleMapper.deleteByProductIds(ids);
|
||||
payProductMapper.deleteWeightBands(ids);
|
||||
payProductMapper.deleteSurcharges(ids);
|
||||
payProductMapper.deleteExtraLines(ids);
|
||||
@@ -1247,176 +1325,6 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return rows == null ? 0 : rows.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PayFeeRuleGroup> listFeeRules(List<Long> productIds)
|
||||
{
|
||||
List<PayFeeRuleGroup> groups = new ArrayList<>();
|
||||
if (productIds == null)
|
||||
{
|
||||
return groups;
|
||||
}
|
||||
LinkedHashSet<Long> ordered = new LinkedHashSet<>();
|
||||
for (Long productId : productIds)
|
||||
{
|
||||
if (productId != null)
|
||||
{
|
||||
ordered.add(productId);
|
||||
}
|
||||
}
|
||||
for (Long productId : ordered)
|
||||
{
|
||||
PayProduct root = requireRoot(productId);
|
||||
PayFeeRuleGroup group = new PayFeeRuleGroup();
|
||||
group.setProductId(root.getProductId());
|
||||
group.setProductName(root.getProductName());
|
||||
List<PayProductFeeRule> rules = payProductFeeRuleMapper.selectByProductId(root.getProductId());
|
||||
group.setRules(rules == null ? new ArrayList<>() : rules);
|
||||
groups.add(group);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveFeeRules(List<PayFeeRuleGroup> products)
|
||||
{
|
||||
if (products == null || products.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择应付产品");
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
Date now = DateUtils.getNowDate();
|
||||
for (PayFeeRuleGroup group : products)
|
||||
{
|
||||
if (group == null || group.getProductId() == null)
|
||||
{
|
||||
throw new ServiceException("请选择应付产品");
|
||||
}
|
||||
PayProduct root = requireRoot(group.getProductId());
|
||||
List<PayProductFeeRule> rules = normalizeFeeRules(root, group.getRules(), operator, now);
|
||||
payProductFeeRuleMapper.deleteByProductId(root.getProductId());
|
||||
if (!rules.isEmpty())
|
||||
{
|
||||
payProductFeeRuleMapper.insertBatch(rules);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PayProduct requireRoot(Long productId)
|
||||
{
|
||||
PayProduct product = payProductMapper.selectPayProductById(productId);
|
||||
if (product == null)
|
||||
{
|
||||
throw new ServiceException("应付产品不存在");
|
||||
}
|
||||
if (product.getOriginId() != null)
|
||||
{
|
||||
product = payProductMapper.selectPayProductById(product.getOriginId());
|
||||
if (product == null)
|
||||
{
|
||||
throw new ServiceException("应付产品不存在");
|
||||
}
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
private List<PayProductFeeRule> normalizeFeeRules(PayProduct root, List<PayProductFeeRule> source, String operator, Date now)
|
||||
{
|
||||
List<PayProductFeeRule> rules = new ArrayList<>();
|
||||
Set<String> names = new HashSet<>();
|
||||
String productName = root.getProductName() == null ? String.valueOf(root.getProductId()) : root.getProductName();
|
||||
if (source == null)
|
||||
{
|
||||
return rules;
|
||||
}
|
||||
for (PayProductFeeRule raw : source)
|
||||
{
|
||||
if (raw == null || isBlank(raw.getFeeName()))
|
||||
{
|
||||
if (raw != null && (feeRuleFilled(raw) || feeRuleChanged(raw)))
|
||||
{
|
||||
throw new ServiceException(productName + "有未选择名称的附加费");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String feeName = raw.getFeeName().trim();
|
||||
if (feeName.length() > 100)
|
||||
{
|
||||
throw new ServiceException(productName + "的附加费名称过长");
|
||||
}
|
||||
if (!names.add(feeName))
|
||||
{
|
||||
throw new ServiceException(productName + "的附加费「" + feeName + "」重复了");
|
||||
}
|
||||
String trigger = isBlank(raw.getTriggerType()) ? "ALWAYS" : raw.getTriggerType().trim().toUpperCase(Locale.ROOT);
|
||||
if (!FEE_TRIGGERS.contains(trigger))
|
||||
{
|
||||
throw new ServiceException(productName + "的触发类型不正确");
|
||||
}
|
||||
String joinMode = isBlank(raw.getJoinMode()) ? "ANY" : raw.getJoinMode().trim().toUpperCase(Locale.ROOT);
|
||||
if (!"ANY".equals(joinMode) && !"ALL".equals(joinMode))
|
||||
{
|
||||
throw new ServiceException(productName + "的命中方式不正确");
|
||||
}
|
||||
String enabled = "1".equals(raw.getEnabled()) ? "1" : "0";
|
||||
checkFeeNumber(raw.getSideMaxCm(), productName, "最长边");
|
||||
checkFeeNumber(raw.getSumMaxCm(), productName, "长宽高之和");
|
||||
checkFeeNumber(raw.getVolumeMaxCm3(), productName, "体积");
|
||||
checkFeeNumber(raw.getMinLengthCm(), productName, "最小长度");
|
||||
checkFeeNumber(raw.getMinWidthCm(), productName, "最小宽度");
|
||||
checkFeeNumber(raw.getMinHeightCm(), productName, "最小高度");
|
||||
checkFeeNumber(raw.getWeightMaxKg(), productName, "重量上限");
|
||||
String remark = raw.getRemark() == null ? null : raw.getRemark().trim();
|
||||
if (remark != null && remark.length() > 500)
|
||||
{
|
||||
throw new ServiceException(productName + "的说明过长");
|
||||
}
|
||||
PayProductFeeRule rule = new PayProductFeeRule();
|
||||
rule.setProductId(root.getProductId());
|
||||
rule.setFeeName(feeName);
|
||||
rule.setEnabled(enabled);
|
||||
rule.setTriggerType(trigger);
|
||||
rule.setSideMaxCm(raw.getSideMaxCm());
|
||||
rule.setSumMaxCm(raw.getSumMaxCm());
|
||||
rule.setVolumeMaxCm3(raw.getVolumeMaxCm3());
|
||||
rule.setMinLengthCm(raw.getMinLengthCm());
|
||||
rule.setMinWidthCm(raw.getMinWidthCm());
|
||||
rule.setMinHeightCm(raw.getMinHeightCm());
|
||||
rule.setWeightMaxKg(raw.getWeightMaxKg());
|
||||
rule.setJoinMode(joinMode);
|
||||
rule.setRemark(isBlank(remark) ? null : remark);
|
||||
rule.setCreateBy(operator);
|
||||
rule.setCreateTime(now);
|
||||
rule.setUpdateBy(operator);
|
||||
rule.setUpdateTime(now);
|
||||
rules.add(rule);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
private static boolean feeRuleChanged(PayProductFeeRule rule)
|
||||
{
|
||||
String trigger = rule.getTriggerType() == null ? "" : rule.getTriggerType().trim();
|
||||
String joinMode = rule.getJoinMode() == null ? "" : rule.getJoinMode().trim();
|
||||
return (!trigger.isEmpty() && !"ALWAYS".equalsIgnoreCase(trigger))
|
||||
|| (!joinMode.isEmpty() && !"ANY".equalsIgnoreCase(joinMode));
|
||||
}
|
||||
|
||||
private static boolean feeRuleFilled(PayProductFeeRule rule)
|
||||
{
|
||||
return rule.getSideMaxCm() != null || rule.getSumMaxCm() != null || rule.getVolumeMaxCm3() != null
|
||||
|| rule.getMinLengthCm() != null || rule.getMinWidthCm() != null || rule.getMinHeightCm() != null
|
||||
|| rule.getWeightMaxKg() != null || !isBlank(rule.getRemark()) || "1".equals(rule.getEnabled());
|
||||
}
|
||||
|
||||
private static void checkFeeNumber(BigDecimal value, String productName, String label)
|
||||
{
|
||||
if (value != null && value.signum() < 0)
|
||||
{
|
||||
throw new ServiceException(productName + "的" + label + "不能小于0");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value)
|
||||
{
|
||||
return value == null || value.trim().isEmpty();
|
||||
@@ -1427,6 +1335,19 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return isBlank(value) ? fallback : value;
|
||||
}
|
||||
|
||||
private static final class ParseSlot
|
||||
{
|
||||
private final PayParseProgress progress;
|
||||
|
||||
private final long at;
|
||||
|
||||
private ParseSlot(PayParseProgress progress, long at)
|
||||
{
|
||||
this.progress = progress;
|
||||
this.at = at;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UploadedSource
|
||||
{
|
||||
private final String path;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<?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.bms.masterdata.mapper.LineProductMapper">
|
||||
|
||||
<resultMap id="LineProductResult" type="com.bms.masterdata.domain.LineProduct">
|
||||
<result property="id" column="id"/>
|
||||
<result property="lineCode" column="line_code"/>
|
||||
<result property="lineName" column="line_name"/>
|
||||
<result property="productType" column="product_type"/>
|
||||
<result property="lineType" column="line_type"/>
|
||||
<result property="blCargo" column="bl_cargo"/>
|
||||
<result property="cargoName" column="cargo_name"/>
|
||||
<result property="productStorage" column="product_storage"/>
|
||||
<result property="productFirst" column="product_first"/>
|
||||
<result property="productClearance" column="product_clearance"/>
|
||||
<result property="productStorageCode" column="product_storage_code"/>
|
||||
<result property="productFirstCode" column="product_first_code"/>
|
||||
<result property="productClearanceCode" column="product_clearance_code"/>
|
||||
<result property="blOpen" column="bl_open"/>
|
||||
<result property="blSection" column="bl_section"/>
|
||||
<result property="dispCode" column="disp_code"/>
|
||||
<result property="dispName" column="disp_name"/>
|
||||
<result property="feeClass" column="fee_class"/>
|
||||
<result property="shipperId" column="shipper_id"/>
|
||||
<result property="shipmentRule" column="shipment_rule"/>
|
||||
<result property="weight" column="weight"/>
|
||||
<result property="cargoType" column="cargo_type"/>
|
||||
<result property="productCategory" column="product_category"/>
|
||||
<result property="volumeBasis" column="volume_basis"/>
|
||||
<result property="weightType" column="weight_type"/>
|
||||
<result property="serviceType" column="service_type"/>
|
||||
<result property="shipmentType" column="shipment_type"/>
|
||||
<result property="taxAmount" column="tax_amount"/>
|
||||
<result property="taxCurrency" column="tax_currency"/>
|
||||
<result property="batteryType" column="battery_type"/>
|
||||
<result property="remark" column="remark"/>
|
||||
<result property="createName" column="create_name"/>
|
||||
<result property="createManCode" column="create_man_code"/>
|
||||
<result property="createDate" column="create_date"/>
|
||||
<result property="modifier" column="modifier"/>
|
||||
<result property="modifierCode" column="modifier_code"/>
|
||||
<result property="modifyDate" column="modify_date"/>
|
||||
<result property="gmtCreate" column="gmt_create"/>
|
||||
<result property="gmtModified" column="gmt_modified"/>
|
||||
<result property="syncTime" column="sync_time"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectLineProductVo">
|
||||
select id, line_code, line_name, product_type, line_type, bl_cargo, cargo_name,
|
||||
product_storage, product_first, product_clearance,
|
||||
product_storage_code, product_first_code, product_clearance_code,
|
||||
bl_open, bl_section, disp_code, disp_name, fee_class, shipper_id, shipment_rule,
|
||||
weight, cargo_type, product_category, volume_basis, weight_type, service_type, shipment_type,
|
||||
tax_amount, tax_currency, battery_type, remark,
|
||||
create_name, create_man_code, create_date, modifier, modifier_code, modify_date,
|
||||
gmt_create, gmt_modified, sync_time
|
||||
from srv_line_product
|
||||
</sql>
|
||||
|
||||
<select id="selectLineProductList" parameterType="com.bms.masterdata.domain.LineProduct" resultMap="LineProductResult">
|
||||
<include refid="selectLineProductVo"/>
|
||||
<where>
|
||||
<if test="lineCode != null and lineCode != ''">
|
||||
AND line_code like concat('%', #{lineCode}, '%')
|
||||
</if>
|
||||
<if test="lineName != null and lineName != ''">
|
||||
AND line_name like concat('%', #{lineName}, '%')
|
||||
</if>
|
||||
<if test="productType != null and productType != ''">
|
||||
AND product_type like concat('%', #{productType}, '%')
|
||||
</if>
|
||||
<if test="lineType != null and lineType != ''">
|
||||
AND line_type like concat('%', #{lineType}, '%')
|
||||
</if>
|
||||
<if test="blOpen != null and blOpen != ''">
|
||||
AND bl_open = #{blOpen}
|
||||
</if>
|
||||
<if test="blCargo != null and blCargo != ''">
|
||||
AND bl_cargo = #{blCargo}
|
||||
</if>
|
||||
<if test="dispName != null and dispName != ''">
|
||||
AND disp_name like concat('%', #{dispName}, '%')
|
||||
</if>
|
||||
<if test="cargoType != null and cargoType != ''">
|
||||
AND cargo_type like concat('%', #{cargoType}, '%')
|
||||
</if>
|
||||
<if test="productCategory != null and productCategory != ''">
|
||||
AND product_category like concat('%', #{productCategory}, '%')
|
||||
</if>
|
||||
</where>
|
||||
order by line_code
|
||||
</select>
|
||||
|
||||
<select id="selectLineProductById" parameterType="String" resultMap="LineProductResult">
|
||||
<include refid="selectLineProductVo"/>
|
||||
where id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectLineProductByCode" parameterType="String" resultMap="LineProductResult">
|
||||
<include refid="selectLineProductVo"/>
|
||||
where line_code = #{lineCode}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="countProductChannelByProductId" parameterType="String" resultType="int">
|
||||
select count(1) from srv_product_channel where product_id = #{productId}
|
||||
</select>
|
||||
|
||||
<insert id="insertLineProduct" parameterType="com.bms.masterdata.domain.LineProduct">
|
||||
insert into srv_line_product (
|
||||
id, line_code, line_name, product_type, line_type, bl_cargo, cargo_name,
|
||||
product_storage, product_first, product_clearance,
|
||||
product_storage_code, product_first_code, product_clearance_code,
|
||||
bl_open, bl_section, disp_code, disp_name, fee_class, shipper_id, shipment_rule,
|
||||
weight, cargo_type, product_category, volume_basis, weight_type, service_type, shipment_type,
|
||||
tax_amount, tax_currency, battery_type, remark,
|
||||
create_name, create_date, modifier, modify_date, gmt_create, gmt_modified
|
||||
) values (
|
||||
#{id}, #{lineCode}, #{lineName}, #{productType}, #{lineType}, #{blCargo}, #{cargoName},
|
||||
#{productStorage}, #{productFirst}, #{productClearance},
|
||||
#{productStorageCode}, #{productFirstCode}, #{productClearanceCode},
|
||||
#{blOpen}, #{blSection}, #{dispCode}, #{dispName}, #{feeClass}, #{shipperId}, #{shipmentRule},
|
||||
#{weight}, #{cargoType}, #{productCategory}, #{volumeBasis}, #{weightType}, #{serviceType}, #{shipmentType},
|
||||
#{taxAmount}, #{taxCurrency}, #{batteryType}, #{remark},
|
||||
#{createName}, sysdate(), #{modifier}, sysdate(), sysdate(), sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateLineProduct" parameterType="com.bms.masterdata.domain.LineProduct">
|
||||
update srv_line_product
|
||||
set line_code = #{lineCode},
|
||||
line_name = #{lineName},
|
||||
product_type = #{productType},
|
||||
line_type = #{lineType},
|
||||
bl_cargo = #{blCargo},
|
||||
cargo_name = #{cargoName},
|
||||
product_storage = #{productStorage},
|
||||
product_first = #{productFirst},
|
||||
product_clearance = #{productClearance},
|
||||
product_storage_code = #{productStorageCode},
|
||||
product_first_code = #{productFirstCode},
|
||||
product_clearance_code = #{productClearanceCode},
|
||||
bl_open = #{blOpen},
|
||||
bl_section = #{blSection},
|
||||
disp_code = #{dispCode},
|
||||
disp_name = #{dispName},
|
||||
fee_class = #{feeClass},
|
||||
shipper_id = #{shipperId},
|
||||
shipment_rule = #{shipmentRule},
|
||||
weight = #{weight},
|
||||
cargo_type = #{cargoType},
|
||||
product_category = #{productCategory},
|
||||
volume_basis = #{volumeBasis},
|
||||
weight_type = #{weightType},
|
||||
service_type = #{serviceType},
|
||||
shipment_type = #{shipmentType},
|
||||
tax_amount = #{taxAmount},
|
||||
tax_currency = #{taxCurrency},
|
||||
battery_type = #{batteryType},
|
||||
remark = #{remark},
|
||||
modifier = #{modifier},
|
||||
modify_date = sysdate(),
|
||||
gmt_modified = sysdate()
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateLineProductStatus" parameterType="com.bms.masterdata.domain.LineProduct">
|
||||
update srv_line_product
|
||||
set bl_open = #{blOpen},
|
||||
modifier = #{modifier},
|
||||
modify_date = sysdate(),
|
||||
gmt_modified = sysdate()
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteLineProductByIds" parameterType="String">
|
||||
delete from srv_line_product where id in
|
||||
<foreach item="id" collection="array" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?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.bms.price.mapper.PayProductFeeRuleMapper">
|
||||
|
||||
<select id="selectByProductId" resultType="com.bms.price.domain.PayProductFeeRule">
|
||||
select rule_id as ruleId, product_id as productId, fee_name as feeName, enabled,
|
||||
trigger_type as triggerType, side_max_cm as sideMaxCm, sum_max_cm as sumMaxCm,
|
||||
volume_max_cm3 as volumeMaxCm3, min_length_cm as minLengthCm, min_width_cm as minWidthCm,
|
||||
min_height_cm as minHeightCm, weight_max_kg as weightMaxKg, join_mode as joinMode,
|
||||
remark, created_by as createBy, create_time as createTime, updated_by as updateBy,
|
||||
update_time as updateTime
|
||||
from pay_product_fee_rule
|
||||
where product_id = #{productId}
|
||||
order by rule_id
|
||||
</select>
|
||||
|
||||
<delete id="deleteByProductId">
|
||||
delete from pay_product_fee_rule where product_id = #{productId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByProductIds">
|
||||
delete from pay_product_fee_rule where product_id in
|
||||
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</delete>
|
||||
|
||||
<insert id="insertBatch">
|
||||
insert into pay_product_fee_rule (
|
||||
product_id, fee_name, enabled, trigger_type, side_max_cm, sum_max_cm, volume_max_cm3,
|
||||
min_length_cm, min_width_cm, min_height_cm, weight_max_kg, join_mode, remark,
|
||||
created_by, create_time, updated_by, update_time
|
||||
) values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.productId}, #{item.feeName}, #{item.enabled}, #{item.triggerType},
|
||||
#{item.sideMaxCm}, #{item.sumMaxCm}, #{item.volumeMaxCm3},
|
||||
#{item.minLengthCm}, #{item.minWidthCm}, #{item.minHeightCm}, #{item.weightMaxKg},
|
||||
#{item.joinMode}, #{item.remark}, #{item.createBy}, #{item.createTime},
|
||||
#{item.updateBy}, #{item.updateTime})
|
||||
</foreach>
|
||||
</insert>
|
||||
</mapper>
|
||||
@@ -0,0 +1,66 @@
|
||||
-- 数据管理 - 路线产品管理
|
||||
-- 权限:data:lineProduct:list|query|add|edit|remove|export
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @parentId := (
|
||||
SELECT menu_id FROM sys_menu
|
||||
WHERE path = 'data' AND menu_type = 'M' AND parent_id = 0
|
||||
ORDER BY menu_id ASC LIMIT 1
|
||||
);
|
||||
|
||||
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 2530, N'路线产品管理', @parentId, 4, 'lineProduct', 'data/lineProduct/index', '', 'LineProduct', 1, 0, 'C', '0', '0', 'data:lineProduct:list', 'list', 'admin', sysdate(), N'路线产品维护,数据表 srv_line_product'
|
||||
FROM DUAL WHERE @parentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2530 OR perms = 'data:lineProduct:list');
|
||||
|
||||
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 2531, N'查询', 2530, 1, '', NULL, 1, 0, 'F', '0', '0', 'data:lineProduct:query', '#', 'admin', sysdate()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2531 OR perms = 'data:lineProduct:query');
|
||||
|
||||
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 2532, N'新增', 2530, 2, '', NULL, 1, 0, 'F', '0', '0', 'data:lineProduct:add', '#', 'admin', sysdate()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2532 OR perms = 'data:lineProduct:add');
|
||||
|
||||
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 2533, N'修改', 2530, 3, '', NULL, 1, 0, 'F', '0', '0', 'data:lineProduct:edit', '#', 'admin', sysdate()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2533 OR perms = 'data:lineProduct:edit');
|
||||
|
||||
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 2534, N'删除', 2530, 4, '', NULL, 1, 0, 'F', '0', '0', 'data:lineProduct:remove', '#', 'admin', sysdate()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2534 OR perms = 'data:lineProduct:remove');
|
||||
|
||||
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 2535, N'导出', 2530, 5, '', NULL, 1, 0, 'F', '0', '0', 'data:lineProduct:export', '#', 'admin', sysdate()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2535 OR perms = 'data:lineProduct:export');
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, m.menu_id FROM (
|
||||
SELECT 2530 AS menu_id UNION SELECT 2531 UNION SELECT 2532 UNION SELECT 2533 UNION SELECT 2534 UNION SELECT 2535
|
||||
) m
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT rm.role_id, dst.menu_id
|
||||
FROM sys_role_menu rm
|
||||
JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'data:serviceChannel:list'
|
||||
JOIN sys_menu dst ON dst.perms IN ('data:lineProduct:list', 'data:lineProduct:query', 'data:lineProduct:export')
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = dst.menu_id
|
||||
);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT rm.role_id, dst.menu_id
|
||||
FROM sys_role_menu rm
|
||||
JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'data:serviceChannel:edit'
|
||||
JOIN sys_menu dst ON dst.perms IN ('data:lineProduct:add', 'data:lineProduct:edit')
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = dst.menu_id
|
||||
);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT rm.role_id, dst.menu_id
|
||||
FROM sys_role_menu rm
|
||||
JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'data:serviceChannel:remove'
|
||||
JOIN sys_menu dst ON dst.perms = 'data:lineProduct:remove'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = dst.menu_id
|
||||
);
|
||||
@@ -88,29 +88,6 @@ CREATE TABLE IF NOT EXISTS `pay_extra_line` (
|
||||
KEY `idx_pay_extra_product` (`product_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品上的邮编或其他配置原文';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_product_fee_rule` (
|
||||
`rule_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '规则ID',
|
||||
`product_id` bigint(20) NOT NULL COMMENT '根产品编号,渠道应付绑定的产品,不是某一版导入的版本号',
|
||||
`fee_name` varchar(100) NOT NULL COMMENT '附加费名称,对应价卡附加费行,用来取价',
|
||||
`enabled` char(1) NOT NULL DEFAULT '0' COMMENT '1参与计算,0不参与',
|
||||
`trigger_type` varchar(20) NOT NULL DEFAULT 'ALWAYS' COMMENT '触发类型:SIZE尺寸 VOLUME体积 WEIGHT重量 POSTCODE邮编 ALWAYS每票 RETURN退件',
|
||||
`side_max_cm` decimal(10,2) DEFAULT NULL COMMENT '最长边上限,厘米,超出则命中',
|
||||
`sum_max_cm` decimal(10,2) DEFAULT NULL COMMENT '长+宽+高上限,厘米,超出则命中',
|
||||
`volume_max_cm3` decimal(14,2) DEFAULT NULL COMMENT '体积上限,立方厘米,长×宽×高超出则命中',
|
||||
`min_length_cm` decimal(10,2) DEFAULT NULL COMMENT '最小长度,厘米,小于则命中',
|
||||
`min_width_cm` decimal(10,2) DEFAULT NULL COMMENT '最小宽度,厘米,小于则命中',
|
||||
`min_height_cm` decimal(10,2) DEFAULT NULL COMMENT '最小高度,厘米,小于则命中',
|
||||
`weight_max_kg` decimal(10,3) DEFAULT NULL COMMENT '重量上限,千克,计费重超出则命中',
|
||||
`join_mode` varchar(10) NOT NULL DEFAULT 'ANY' COMMENT 'ANY任一条超出就收,ALL写了的条件都超出才收',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '说明,不参与判断',
|
||||
`created_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`rule_id`),
|
||||
UNIQUE KEY `uk_pay_fee_rule_product` (`product_id`, `fee_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品附加费开关';
|
||||
|
||||
-- 菜单挂在价格基础下面
|
||||
SET @parentId := (
|
||||
SELECT menu_id FROM sys_menu
|
||||
|
||||
@@ -1,25 +1,4 @@
|
||||
-- 应付产品附加费开关。挂在根产品上,同一产品同一费用名称一行。可重复执行。
|
||||
-- 应付附加费开关已取消。价格和计算条件都在价卡附加费上。可重复执行。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_product_fee_rule` (
|
||||
`rule_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '规则ID',
|
||||
`product_id` bigint(20) NOT NULL COMMENT '根产品编号,渠道应付绑定的产品,不是某一版导入的版本号',
|
||||
`fee_name` varchar(100) NOT NULL COMMENT '附加费名称,对应价卡附加费行,用来取价',
|
||||
`enabled` char(1) NOT NULL DEFAULT '0' COMMENT '1参与计算,0不参与',
|
||||
`trigger_type` varchar(20) NOT NULL DEFAULT 'ALWAYS' COMMENT '触发类型:SIZE尺寸 VOLUME体积 WEIGHT重量 POSTCODE邮编 ALWAYS每票 RETURN退件',
|
||||
`side_max_cm` decimal(10,2) DEFAULT NULL COMMENT '最长边上限,厘米,超出则命中',
|
||||
`sum_max_cm` decimal(10,2) DEFAULT NULL COMMENT '长+宽+高上限,厘米,超出则命中',
|
||||
`volume_max_cm3` decimal(14,2) DEFAULT NULL COMMENT '体积上限,立方厘米,长×宽×高超出则命中',
|
||||
`min_length_cm` decimal(10,2) DEFAULT NULL COMMENT '最小长度,厘米,小于则命中',
|
||||
`min_width_cm` decimal(10,2) DEFAULT NULL COMMENT '最小宽度,厘米,小于则命中',
|
||||
`min_height_cm` decimal(10,2) DEFAULT NULL COMMENT '最小高度,厘米,小于则命中',
|
||||
`weight_max_kg` decimal(10,3) DEFAULT NULL COMMENT '重量上限,千克,计费重超出则命中',
|
||||
`join_mode` varchar(10) NOT NULL DEFAULT 'ANY' COMMENT 'ANY任一条超出就收,ALL写了的条件都超出才收',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '说明,不参与判断',
|
||||
`created_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`rule_id`),
|
||||
UNIQUE KEY `uk_pay_fee_rule_product` (`product_id`, `fee_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品附加费开关';
|
||||
DROP TABLE IF EXISTS `pay_product_fee_rule`;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listLineProduct(query) {
|
||||
return request({
|
||||
url: '/masterdata/lineproduct/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getLineProduct(id) {
|
||||
return request({
|
||||
url: '/masterdata/lineproduct/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addLineProduct(data) {
|
||||
return request({
|
||||
url: '/masterdata/lineproduct',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateLineProduct(data) {
|
||||
return request({
|
||||
url: '/masterdata/lineproduct',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function changeLineProductStatus(id, blOpen) {
|
||||
return request({
|
||||
url: '/masterdata/lineproduct/changeStatus',
|
||||
method: 'put',
|
||||
data: { id, blOpen }
|
||||
})
|
||||
}
|
||||
|
||||
export function delLineProduct(ids) {
|
||||
return request({
|
||||
url: '/masterdata/lineproduct/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -19,18 +19,30 @@ export function getPay(productId) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 按字典匹配工作表,返回预览,不写数据库 */
|
||||
export function previewPay(file, dictValue) {
|
||||
/** 按字典匹配工作表,返回预览,不写数据库。progressId 用来轮询解析进度。 */
|
||||
export function previewPay(file, dictValue, progressId, onUploadProgress) {
|
||||
const data = new FormData()
|
||||
data.append('file', file)
|
||||
if (dictValue) data.append('dictValue', dictValue)
|
||||
if (progressId) data.append('progressId', progressId)
|
||||
return request({
|
||||
url: '/price/pay/preview',
|
||||
method: 'post',
|
||||
data,
|
||||
// 必须声明 multipart,否则全局 application/json 会把文件流改写成空 JSON
|
||||
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
|
||||
timeout: 10 * 60 * 1000
|
||||
timeout: 10 * 60 * 1000,
|
||||
onUploadProgress
|
||||
})
|
||||
}
|
||||
|
||||
/** 查询一次价卡解析进度 */
|
||||
export function payParseProgress(progressId) {
|
||||
return request({
|
||||
url: '/price/pay/preview/progress',
|
||||
method: 'get',
|
||||
params: { progressId },
|
||||
headers: { repeatSubmit: false }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,24 +152,6 @@ export function generatePayBill(billId) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 勾选产品的附加费开关。productIds 用逗号分隔 */
|
||||
export function listFeeRule(productIds) {
|
||||
return request({
|
||||
url: '/price/pay/feeRule/list',
|
||||
method: 'get',
|
||||
params: { productIds: productIds.join(',') }
|
||||
})
|
||||
}
|
||||
|
||||
/** 按产品保存附加费开关。每个产品一份 */
|
||||
export function saveFeeRule(data) {
|
||||
return request({
|
||||
url: '/price/pay/feeRule',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除产品及其重量段、附加费、其他配置 */
|
||||
export function delPay(productIds) {
|
||||
return request({
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
row-key="id"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['data:lineProduct:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['data:lineProduct:export']">导出</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<template #col-blOpen="{ row }">{{ row.blOpen === '1' ? '启用' : row.blOpen === '0' ? '停用' : '-' }}</template>
|
||||
<template #col-blCargo="{ row }">{{ yesNoText(row.blCargo) }}</template>
|
||||
<template #col-blSection="{ row }">{{ yesNoText(row.blSection) }}</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleDetail(row)" v-hasPermi="['data:lineProduct:query']">详情</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-dialog :title="title" v-model="open" width="980px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" :disabled="viewing" label-width="120px" class="line-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品编码" prop="lineCode">
|
||||
<el-input v-model="form.lineCode" maxlength="64" placeholder="请输入产品编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品名称" prop="lineName">
|
||||
<el-input v-model="form.lineName" maxlength="200" placeholder="请输入产品名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品类型" prop="productType">
|
||||
<el-input v-model="form.productType" maxlength="20" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="线路类型" prop="lineType">
|
||||
<el-input v-model="form.lineType" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品分类" prop="productCategory">
|
||||
<el-input v-model="form.productCategory" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="货物类型" prop="cargoType">
|
||||
<el-input v-model="form.cargoType" maxlength="64" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="货物名称" prop="cargoName">
|
||||
<el-input v-model="form.cargoName" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="费用类别" prop="feeClass">
|
||||
<el-input v-model="form.feeClass" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="启用状态" prop="blOpen">
|
||||
<el-radio-group v-model="form.blOpen">
|
||||
<el-radio value="1">启用</el-radio>
|
||||
<el-radio value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="是否可收货" prop="blCargo">
|
||||
<el-radio-group v-model="form.blCargo">
|
||||
<el-radio value="1">是</el-radio>
|
||||
<el-radio value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="是否分段" prop="blSection">
|
||||
<el-radio-group v-model="form.blSection">
|
||||
<el-radio value="1">是</el-radio>
|
||||
<el-radio value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="派送商编码" prop="dispCode">
|
||||
<el-input v-model="form.dispCode" maxlength="60" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="派送商名称" prop="dispName">
|
||||
<el-input v-model="form.dispName" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发货人ID" prop="shipperId">
|
||||
<el-input v-model="form.shipperId" maxlength="64" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发货规则" prop="shipmentRule">
|
||||
<el-input v-model="form.shipmentRule" maxlength="1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发货类型" prop="shipmentType">
|
||||
<el-input v-model="form.shipmentType" maxlength="32" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="服务类型" prop="serviceType">
|
||||
<el-input v-model="form.serviceType" maxlength="1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="重量类型" prop="weightType">
|
||||
<el-input v-model="form.weightType" maxlength="1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="限重(kg)" prop="weight">
|
||||
<el-input-number v-model="form.weight" :min="0" :precision="2" :value-on-clear="null" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="体积基数" prop="volumeBasis">
|
||||
<el-input-number v-model="form.volumeBasis" :min="0" :precision="2" :value-on-clear="null" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="税费金额" prop="taxAmount">
|
||||
<el-input-number v-model="form.taxAmount" :min="0" :precision="3" :value-on-clear="null" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="税费币种" prop="taxCurrency">
|
||||
<el-input v-model="form.taxCurrency" maxlength="16" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="电池类型" prop="batteryType">
|
||||
<el-input v-model="form.batteryType" maxlength="64" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="仓储服务名称" prop="productStorage">
|
||||
<el-input v-model="form.productStorage" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="仓储服务编码" prop="productStorageCode">
|
||||
<el-input v-model="form.productStorageCode" type="textarea" :rows="2" maxlength="1000" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="头程服务名称" prop="productFirst">
|
||||
<el-input v-model="form.productFirst" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="头程服务编码" prop="productFirstCode">
|
||||
<el-input v-model="form.productFirstCode" type="textarea" :rows="2" maxlength="1000" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="清关服务名称" prop="productClearance">
|
||||
<el-input v-model="form.productClearance" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="清关服务编码" prop="productClearanceCode">
|
||||
<el-input v-model="form.productClearanceCode" type="textarea" :rows="2" maxlength="1000" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button v-if="!viewing" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="open = false">{{ viewing ? '关 闭' : '取 消' }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="LineProduct">
|
||||
import { nextTick } from 'vue'
|
||||
import {
|
||||
listLineProduct, getLineProduct, addLineProduct
|
||||
} from '@/api/masterdata/lineProduct'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const formRef = ref()
|
||||
const open = ref(false)
|
||||
const viewing = ref(false)
|
||||
const title = ref('')
|
||||
|
||||
const openOptions = [
|
||||
{ label: '启用', value: '1' },
|
||||
{ label: '停用', value: '0' }
|
||||
]
|
||||
const yesNoOptions = [
|
||||
{ label: '是', value: '1' },
|
||||
{ label: '否', value: '0' }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
{ label: '产品编码', prop: 'lineCode', component: 'input', placeholder: '产品编码', width: '160px' },
|
||||
{ label: '产品名称', prop: 'lineName', component: 'input', placeholder: '模糊查询', width: '180px' },
|
||||
{ label: '产品类型', prop: 'productType', component: 'input', placeholder: '模糊查询', width: '150px' },
|
||||
{ label: '线路类型', prop: 'lineType', component: 'input', placeholder: '模糊查询', width: '150px' },
|
||||
{ label: '派送商', prop: 'dispName', component: 'input', placeholder: '模糊查询', width: '160px' },
|
||||
{ label: '货物类型', prop: 'cargoType', component: 'input', placeholder: '模糊查询', width: '150px' },
|
||||
{ label: '产品分类', prop: 'productCategory', component: 'input', placeholder: '模糊查询', width: '150px' },
|
||||
{ label: '启用', prop: 'blOpen', component: 'select', options: openOptions, placeholder: '启用状态', width: '130px', props: { clearable: true } },
|
||||
{ label: '可收货', prop: 'blCargo', component: 'select', options: yesNoOptions, placeholder: '是否可收货', width: '130px', props: { clearable: true } }
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ label: '产品编码', prop: 'lineCode', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '产品名称', prop: 'lineName', minWidth: 180, showOverflowTooltip: true },
|
||||
{ label: '产品类型', prop: 'productType', minWidth: 110, showOverflowTooltip: true },
|
||||
{ label: '线路类型', prop: 'lineType', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '可收货', prop: 'blCargo', width: 80, slot: 'blCargo' },
|
||||
{ label: '货物名称', prop: 'cargoName', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '仓储服务名称', prop: 'productStorage', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '头程服务名称', prop: 'productFirst', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '清关服务名称', prop: 'productClearance', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '仓储服务编码', prop: 'productStorageCode', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '头程服务编码', prop: 'productFirstCode', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '清关服务编码', prop: 'productClearanceCode', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '启用', prop: 'blOpen', width: 80, slot: 'blOpen' },
|
||||
{ label: '分段', prop: 'blSection', width: 70, slot: 'blSection' },
|
||||
{ label: '派送商编码', prop: 'dispCode', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '派送商名称', prop: 'dispName', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '费用类别', prop: 'feeClass', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '发货人ID', prop: 'shipperId', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '发货规则', prop: 'shipmentRule', width: 90 },
|
||||
{ label: '限重(kg)', prop: 'weight', width: 100 },
|
||||
{ label: '货物类型', prop: 'cargoType', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '产品分类', prop: 'productCategory', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '体积基数', prop: 'volumeBasis', width: 100 },
|
||||
{ label: '重量类型', prop: 'weightType', width: 90 },
|
||||
{ label: '服务类型', prop: 'serviceType', width: 90 },
|
||||
{ label: '发货类型', prop: 'shipmentType', minWidth: 110, showOverflowTooltip: true },
|
||||
{ label: '税费金额', prop: 'taxAmount', width: 100 },
|
||||
{ label: '税费币种', prop: 'taxCurrency', width: 90 },
|
||||
{ label: '电池类型', prop: 'batteryType', minWidth: 110, showOverflowTooltip: true },
|
||||
{ label: '备注', prop: 'remark', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '创建时间', prop: 'createDate', width: 170, parseTime: true },
|
||||
{ label: '修改人', prop: 'modifier', minWidth: 100, showOverflowTooltip: true },
|
||||
{ label: '修改人工号', prop: 'modifierCode', minWidth: 110, showOverflowTooltip: true },
|
||||
{ label: '修改时间', prop: 'modifyDate', width: 170, parseTime: true },
|
||||
{ label: 'GMT创建时间', prop: 'gmtCreate', width: 170, parseTime: true },
|
||||
{ label: 'GMT修改时间', prop: 'gmtModified', width: 170, parseTime: true },
|
||||
{ label: '同步时间', prop: 'syncTime', width: 170, parseTime: true },
|
||||
{ label: '操作', prop: 'action', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
const data = reactive({
|
||||
form: {},
|
||||
rules: {
|
||||
lineCode: [{ required: true, message: '产品编码不能为空', trigger: 'blur' }],
|
||||
lineName: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
|
||||
blOpen: [{ required: true, message: '请选择启用状态', trigger: 'change' }]
|
||||
}
|
||||
})
|
||||
const { form, rules } = toRefs(data)
|
||||
|
||||
function queryList(params) {
|
||||
return listLineProduct(params)
|
||||
}
|
||||
|
||||
function yesNoText(value) {
|
||||
if (value === '1') return '是'
|
||||
if (value === '0') return '否'
|
||||
return '-'
|
||||
}
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
id: undefined,
|
||||
lineCode: '',
|
||||
lineName: '',
|
||||
productType: '',
|
||||
lineType: '',
|
||||
blCargo: '1',
|
||||
cargoName: '',
|
||||
productStorage: '',
|
||||
productFirst: '',
|
||||
productClearance: '',
|
||||
productStorageCode: '',
|
||||
productFirstCode: '',
|
||||
productClearanceCode: '',
|
||||
blOpen: '1',
|
||||
blSection: '0',
|
||||
dispCode: '',
|
||||
dispName: '',
|
||||
feeClass: '',
|
||||
shipperId: '',
|
||||
shipmentRule: '',
|
||||
weight: undefined,
|
||||
cargoType: '',
|
||||
productCategory: '',
|
||||
volumeBasis: undefined,
|
||||
weightType: '',
|
||||
serviceType: '',
|
||||
shipmentType: '',
|
||||
taxAmount: undefined,
|
||||
taxCurrency: '',
|
||||
batteryType: '',
|
||||
remark: ''
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
viewing.value = false
|
||||
form.value = emptyForm()
|
||||
title.value = '新增路线产品'
|
||||
open.value = true
|
||||
nextTick(() => formRef.value && formRef.value.clearValidate())
|
||||
}
|
||||
|
||||
function handleDetail(row) {
|
||||
getLineProduct(row.id).then(res => {
|
||||
viewing.value = true
|
||||
form.value = Object.assign(emptyForm(), res.data || {})
|
||||
title.value = '路线产品详情'
|
||||
open.value = true
|
||||
nextTick(() => formRef.value && formRef.value.clearValidate())
|
||||
})
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
if (viewing.value) return
|
||||
formRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
addLineProduct(form.value).then(() => {
|
||||
proxy.$modal.msgSuccess('保存成功')
|
||||
open.value = false
|
||||
tableRef.value?.refresh()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
proxy.download('masterdata/lineproduct/export', {
|
||||
...tableRef.value.getQueryParams()
|
||||
}, `line_product_${new Date().getTime()}.xlsx`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-form {
|
||||
max-height: 68vh;
|
||||
overflow: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -36,13 +36,14 @@
|
||||
<span>{{ row.blMultiple === '1' ? '是' : row.blMultiple === '0' ? '否' : '-' }}</span>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleDetail(row)" v-hasPermi="['data:serviceChannel:query']">详情</el-button>
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['data:serviceChannel:edit']">修改</el-button>
|
||||
<el-button link type="primary" icon="Delete" @click="handleDelete([row.id])" v-hasPermi="['data:serviceChannel:remove']">删除</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-dialog :title="title" v-model="open" width="760px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form ref="formRef" :model="form" :rules="viewing ? {} : rules" :disabled="viewing" label-width="120px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="渠道编码" prop="channelCode">
|
||||
@@ -142,7 +143,7 @@
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="应付产品">
|
||||
<el-select v-model="payOriginId" filterable clearable placeholder="选择已导入的应付产品" style="width: 280px" :disabled="!form.id" @change="onPayOrigin">
|
||||
<el-select v-model="payOriginId" filterable clearable placeholder="选择已导入的应付产品" style="width: 280px" :disabled="viewing || !form.id" @change="onPayOrigin">
|
||||
<el-option v-for="item in payOptions" :key="item.productId" :label="payOptionLabel(item)" :value="item.productId" />
|
||||
</el-select>
|
||||
<div class="pay-tip">{{ payTip }}</div>
|
||||
@@ -151,8 +152,8 @@
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="open = false">取 消</el-button>
|
||||
<el-button v-if="!viewing" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="open = false">{{ viewing ? '关 闭' : '取 消' }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -171,12 +172,17 @@ const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const formRef = ref()
|
||||
const open = ref(false)
|
||||
const viewing = ref(false)
|
||||
const title = ref('')
|
||||
const payOptions = ref([])
|
||||
const payOriginId = ref()
|
||||
const payProduct = ref(null)
|
||||
const syncingPay = ref(false)
|
||||
const payTip = computed(() => {
|
||||
if (viewing.value) {
|
||||
if (payProduct.value?.versionNo) return `已绑定第 ${payProduct.value.versionNo} 版。`
|
||||
return payOriginId.value ? '已绑定应付产品。' : '未绑定应付产品。'
|
||||
}
|
||||
if (!form.value?.id) return '先保存渠道,再选择应付产品。渠道绑定的是产品编号,更新价格不会换编号。'
|
||||
if (payProduct.value?.versionNo) return `当前按有效期使用第 ${payProduct.value.versionNo} 版。历史订单按下单或计费时间取当时的版本。`
|
||||
return '选择产品后即绑定,按应付 AP 保存。'
|
||||
@@ -210,7 +216,7 @@ const columns = [
|
||||
{ label: '末端服务商', prop: 'finalServiceName', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '抛货系数', prop: 'dimDivisor', width: 90 },
|
||||
{ label: '同步时间', prop: 'syncTime', width: 170, parseTime: true },
|
||||
{ label: '操作', prop: 'action', width: 150, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
{ label: '操作', prop: 'action', width: 200, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
const data = reactive({
|
||||
@@ -285,6 +291,7 @@ function loadChannelPay(channelId) {
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
viewing.value = false
|
||||
form.value = emptyForm()
|
||||
resetPay()
|
||||
loadPayOptions()
|
||||
@@ -295,6 +302,7 @@ function handleAdd() {
|
||||
|
||||
function handleUpdate(row) {
|
||||
getServiceChannel(row.id).then(res => {
|
||||
viewing.value = false
|
||||
form.value = Object.assign(emptyForm(), res.data || {})
|
||||
resetPay()
|
||||
loadPayOptions()
|
||||
@@ -305,8 +313,21 @@ function handleUpdate(row) {
|
||||
})
|
||||
}
|
||||
|
||||
function handleDetail(row) {
|
||||
getServiceChannel(row.id).then(res => {
|
||||
viewing.value = true
|
||||
form.value = Object.assign(emptyForm(), res.data || {})
|
||||
resetPay()
|
||||
loadPayOptions()
|
||||
loadChannelPay(row.id)
|
||||
title.value = '渠道详情'
|
||||
open.value = true
|
||||
nextTick(() => formRef.value && formRef.value.clearValidate())
|
||||
})
|
||||
}
|
||||
|
||||
function onPayOrigin(id) {
|
||||
if (!form.value.id || syncingPay.value) return
|
||||
if (viewing.value || !form.value.id || syncingPay.value) return
|
||||
if (!id) {
|
||||
payProduct.value = null
|
||||
bindChannelPay(form.value.id, null, 'AP')
|
||||
@@ -319,6 +340,7 @@ function onPayOrigin(id) {
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
if (viewing.value) return
|
||||
formRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
const req = form.value.id ? updateServiceChannel(form.value) : addServiceChannel(form.value)
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="附加费配置"
|
||||
width="1280px"
|
||||
top="6vh"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="reset"
|
||||
>
|
||||
<div v-loading="loading">
|
||||
<p class="fee-tip">还没配置的产品共用一页,保存后按产品各写一份。已经配置过的产品单独一页,互不影响。</p>
|
||||
<el-tabs v-if="tabs.length > 1" v-model="active">
|
||||
<el-tab-pane v-for="tab in tabs" :key="tab.key" :label="tab.label" :name="tab.key" />
|
||||
</el-tabs>
|
||||
<div v-if="current" class="fee-bar">
|
||||
<span>{{ currentHint }}</span>
|
||||
<el-button type="primary" plain icon="Plus" @click="addRow">添加一行</el-button>
|
||||
</div>
|
||||
<el-table v-if="current" :data="current.rules" border size="small" max-height="460" empty-text="还没有附加费">
|
||||
<el-table-column label="附加费名称" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.feeName" filterable clearable placeholder="选择附加费">
|
||||
<el-option v-for="item in feeOptions(row)" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" active-value="1" inactive-value="0" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="触发类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.triggerType">
|
||||
<el-option v-for="item in triggerOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最长边cm" width="108">
|
||||
<template #default="{ row }"><el-input-number v-model="row.sideMaxCm" :min="0" :controls="false" :value-on-clear="null" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="长+宽+高cm" width="116">
|
||||
<template #default="{ row }"><el-input-number v-model="row.sumMaxCm" :min="0" :controls="false" :value-on-clear="null" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="体积cm³" width="116">
|
||||
<template #default="{ row }"><el-input-number v-model="row.volumeMaxCm3" :min="0" :controls="false" :value-on-clear="null" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最小长" width="96">
|
||||
<template #default="{ row }"><el-input-number v-model="row.minLengthCm" :min="0" :controls="false" :value-on-clear="null" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最小宽" width="96">
|
||||
<template #default="{ row }"><el-input-number v-model="row.minWidthCm" :min="0" :controls="false" :value-on-clear="null" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最小高" width="96">
|
||||
<template #default="{ row }"><el-input-number v-model="row.minHeightCm" :min="0" :controls="false" :value-on-clear="null" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重量上限kg" width="112">
|
||||
<template #default="{ row }"><el-input-number v-model="row.weightMaxKg" :min="0" :precision="3" :controls="false" :value-on-clear="null" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="命中方式" width="128">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.joinMode">
|
||||
<el-option label="任一超出" value="ANY" />
|
||||
<el-option label="全部超出" value="ALL" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" maxlength="500" placeholder="不参与判断" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="70" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="removeRow($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listFeeRule, saveFeeRule } from '@/api/price/pay'
|
||||
import { useDict } from '@/utils/dict'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_surcharge_type } = useDict('bms_surcharge_type')
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const tabs = ref([])
|
||||
const active = ref('')
|
||||
|
||||
const triggerOptions = [
|
||||
{ label: '尺寸', value: 'SIZE' },
|
||||
{ label: '体积', value: 'VOLUME' },
|
||||
{ label: '重量', value: 'WEIGHT' },
|
||||
{ label: '邮编', value: 'POSTCODE' },
|
||||
{ label: '每票', value: 'ALWAYS' },
|
||||
{ label: '退件', value: 'RETURN' }
|
||||
]
|
||||
|
||||
const current = computed(() => tabs.value.find(tab => tab.key === active.value) || null)
|
||||
const currentHint = computed(() => {
|
||||
if (!current.value) return ''
|
||||
if (current.value.productIds.length > 1) return `同时写入:${current.value.productName}`
|
||||
return tabs.value.length === 1 ? current.value.productName : ''
|
||||
})
|
||||
|
||||
function blankRule() {
|
||||
return {
|
||||
feeName: '',
|
||||
enabled: '0',
|
||||
triggerType: 'ALWAYS',
|
||||
sideMaxCm: null,
|
||||
sumMaxCm: null,
|
||||
volumeMaxCm3: null,
|
||||
minLengthCm: null,
|
||||
minWidthCm: null,
|
||||
minHeightCm: null,
|
||||
weightMaxKg: null,
|
||||
joinMode: 'ANY',
|
||||
remark: ''
|
||||
}
|
||||
}
|
||||
|
||||
function copyRule(row) {
|
||||
const next = blankRule()
|
||||
next.feeName = row.feeName || ''
|
||||
next.enabled = row.enabled === '1' ? '1' : '0'
|
||||
next.triggerType = row.triggerType || 'ALWAYS'
|
||||
next.sideMaxCm = toNumber(row.sideMaxCm)
|
||||
next.sumMaxCm = toNumber(row.sumMaxCm)
|
||||
next.volumeMaxCm3 = toNumber(row.volumeMaxCm3)
|
||||
next.minLengthCm = toNumber(row.minLengthCm)
|
||||
next.minWidthCm = toNumber(row.minWidthCm)
|
||||
next.minHeightCm = toNumber(row.minHeightCm)
|
||||
next.weightMaxKg = toNumber(row.weightMaxKg)
|
||||
next.joinMode = row.joinMode || 'ANY'
|
||||
next.remark = row.remark || ''
|
||||
return next
|
||||
}
|
||||
|
||||
function toNumber(value) {
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number : null
|
||||
}
|
||||
|
||||
function feeOptions(row) {
|
||||
const list = bms_surcharge_type.value || []
|
||||
if (row.feeName && !list.some(item => item.value === row.feeName)) {
|
||||
return [{ label: row.feeName, value: row.feeName }, ...list]
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
current.value?.rules.push(blankRule())
|
||||
}
|
||||
|
||||
function removeRow(index) {
|
||||
if (!current.value) return
|
||||
current.value.rules.splice(index, 1)
|
||||
if (!current.value.rules.length) current.value.rules.push(blankRule())
|
||||
}
|
||||
|
||||
function rowTouched(row) {
|
||||
return !!(row.feeName || row.remark || row.enabled === '1'
|
||||
|| (row.triggerType && row.triggerType !== 'ALWAYS')
|
||||
|| (row.joinMode && row.joinMode !== 'ANY')
|
||||
|| [row.sideMaxCm, row.sumMaxCm, row.volumeMaxCm3, row.minLengthCm, row.minWidthCm, row.minHeightCm, row.weightMaxKg]
|
||||
.some(value => value !== null && value !== undefined && value !== ''))
|
||||
}
|
||||
|
||||
function keptRules(tab) {
|
||||
const kept = []
|
||||
const names = new Set()
|
||||
for (const row of tab.rules) {
|
||||
if (!rowTouched(row)) continue
|
||||
const feeName = (row.feeName || '').trim()
|
||||
if (!feeName) {
|
||||
proxy.$modal.msgError(`${tab.productName || '产品'}有未选择名称的附加费`)
|
||||
return null
|
||||
}
|
||||
if (names.has(feeName)) {
|
||||
proxy.$modal.msgError(`${tab.productName || '产品'}的附加费「${feeName}」重复了`)
|
||||
return null
|
||||
}
|
||||
names.add(feeName)
|
||||
kept.push({ ...row, feeName, remark: (row.remark || '').trim() })
|
||||
}
|
||||
return kept
|
||||
}
|
||||
|
||||
function save() {
|
||||
const products = []
|
||||
for (const tab of tabs.value) {
|
||||
const rules = keptRules(tab)
|
||||
if (!rules) return
|
||||
for (const productId of tab.productIds) {
|
||||
products.push({ productId, rules: rules.map(row => ({ ...row })) })
|
||||
}
|
||||
}
|
||||
if (!products.length) return
|
||||
saving.value = true
|
||||
saveFeeRule(products).then(() => {
|
||||
proxy.$modal.msgSuccess('附加费配置已保存')
|
||||
visible.value = false
|
||||
}).finally(() => {
|
||||
saving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function reset() {
|
||||
tabs.value = []
|
||||
active.value = ''
|
||||
loading.value = false
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
function open(rows) {
|
||||
const picked = (rows || []).filter(row => row && row.productId != null)
|
||||
if (!picked.length) return
|
||||
reset()
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
listFeeRule(picked.map(row => row.productId)).then(res => {
|
||||
const groups = res.data || []
|
||||
const unbound = groups.filter(group => !group.rules?.length)
|
||||
const bound = groups.filter(group => group.rules?.length)
|
||||
const next = []
|
||||
if (unbound.length) {
|
||||
const names = unbound.map(group => group.productName || `产品${group.productId}`)
|
||||
next.push({
|
||||
key: 'unbound',
|
||||
label: '未配置',
|
||||
productIds: unbound.map(group => group.productId),
|
||||
productName: names.join('、'),
|
||||
rules: [blankRule()]
|
||||
})
|
||||
}
|
||||
const nameCount = bound.reduce((map, group) => {
|
||||
const name = group.productName || ''
|
||||
map.set(name, (map.get(name) || 0) + 1)
|
||||
return map
|
||||
}, new Map())
|
||||
bound.forEach(group => {
|
||||
const name = group.productName || `产品${group.productId}`
|
||||
next.push({
|
||||
key: `bound-${group.productId}`,
|
||||
label: nameCount.get(group.productName || '') > 1 ? `${name}(${group.productId})` : name,
|
||||
productIds: [group.productId],
|
||||
productName: name,
|
||||
rules: group.rules.map(copyRule)
|
||||
})
|
||||
})
|
||||
tabs.value = next
|
||||
active.value = next.length ? next[0].key : ''
|
||||
}).catch(() => {
|
||||
visible.value = false
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fee-tip {
|
||||
margin: 0 0 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.fee-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -28,6 +28,18 @@
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<div v-if="recognizing" class="parse-progress">
|
||||
<el-progress
|
||||
:percentage="parsePercent"
|
||||
:stroke-width="16"
|
||||
:status="parseStatus"
|
||||
striped
|
||||
striped-flow
|
||||
:duration="12"
|
||||
/>
|
||||
<div class="parse-progress-msg">{{ parseMessage }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="preview" v-loading="recognizing" class="preview-box">
|
||||
<el-alert
|
||||
:title="`${preview.sourceFile || ''} 归属 ${preview.supplierName || '-'} 生效 ${preview.effectiveDate || '-'} 共 ${products.length} 个产品`"
|
||||
@@ -97,7 +109,7 @@
|
||||
<script setup>
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { previewPay, savePay } from '@/api/price/pay'
|
||||
import { previewPay, payParseProgress, savePay } from '@/api/price/pay'
|
||||
import { useDict } from '@/utils/dict'
|
||||
import PayConfigDetail from './PayConfigDetail.vue'
|
||||
|
||||
@@ -116,17 +128,24 @@ const selected = ref([])
|
||||
const tableRef = ref()
|
||||
const activeKey = ref('')
|
||||
const uploadKey = ref(0)
|
||||
const parsePercent = ref(0)
|
||||
const parseMessage = ref('')
|
||||
const parseStatus = ref(undefined)
|
||||
|
||||
const products = computed(() => preview.value?.products || [])
|
||||
const activeProduct = computed(() => products.value.find(item => item.rowKey === activeKey.value) || products.value[0])
|
||||
const skippedText = computed(() => (preview.value?.skippedSheets || []).filter(Boolean).join('、'))
|
||||
|
||||
let parseSeq = 0
|
||||
let progressTimer = null
|
||||
|
||||
function open() {
|
||||
reset()
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
function reset() {
|
||||
parseSeq += 1
|
||||
preview.value = null
|
||||
selected.value = []
|
||||
overwrite.value = false
|
||||
@@ -135,6 +154,10 @@ function reset() {
|
||||
activeKey.value = ''
|
||||
recognizing.value = false
|
||||
saving.value = false
|
||||
parsePercent.value = 0
|
||||
parseMessage.value = ''
|
||||
parseStatus.value = undefined
|
||||
stopProgress()
|
||||
uploadKey.value += 1
|
||||
}
|
||||
|
||||
@@ -145,9 +168,26 @@ async function onFile(uploadFile) {
|
||||
proxy.$modal.msgError('请上传xlsx价卡')
|
||||
return
|
||||
}
|
||||
const seq = ++parseSeq
|
||||
const progressId = createProgressId()
|
||||
recognizing.value = true
|
||||
parsePercent.value = 0
|
||||
parseMessage.value = '正在上传价卡…'
|
||||
parseStatus.value = undefined
|
||||
startProgress(seq, progressId)
|
||||
try {
|
||||
const res = await previewPay(raw, dictValue.value)
|
||||
const res = await previewPay(raw, dictValue.value, progressId, event => {
|
||||
if (seq !== parseSeq || !event?.total) return
|
||||
const uploaded = Math.min(8, Math.round(event.loaded * 8 / event.total))
|
||||
if (uploaded > parsePercent.value) {
|
||||
parsePercent.value = uploaded
|
||||
parseMessage.value = '正在上传价卡…'
|
||||
}
|
||||
})
|
||||
if (seq !== parseSeq) return
|
||||
parsePercent.value = 100
|
||||
parseMessage.value = '解析完成'
|
||||
parseStatus.value = 'success'
|
||||
const data = res.data || {}
|
||||
data.products = (data.products || []).map((item, index) => ({
|
||||
...item,
|
||||
@@ -160,8 +200,48 @@ async function onFile(uploadFile) {
|
||||
await nextTick()
|
||||
tableRef.value?.clearSelection()
|
||||
picked.forEach(row => tableRef.value?.toggleRowSelection(row, true))
|
||||
} catch (error) {
|
||||
if (seq !== parseSeq) return
|
||||
parseStatus.value = 'exception'
|
||||
parseMessage.value = error?.message || '价卡解析失败'
|
||||
} finally {
|
||||
recognizing.value = false
|
||||
if (seq === parseSeq) {
|
||||
stopProgress()
|
||||
recognizing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createProgressId() {
|
||||
if (window.crypto?.randomUUID) return window.crypto.randomUUID()
|
||||
return `pay${Date.now().toString(16)}${Math.random().toString(16).slice(2, 10)}`
|
||||
}
|
||||
|
||||
function startProgress(seq, progressId) {
|
||||
stopProgress()
|
||||
progressTimer = window.setInterval(async () => {
|
||||
if (seq !== parseSeq) return
|
||||
try {
|
||||
const res = await payParseProgress(progressId)
|
||||
if (seq !== parseSeq) return
|
||||
const data = res.data || {}
|
||||
if (!data.status || data.status === 'PENDING') return
|
||||
const server = Math.min(99, Number(data.percent) || 0)
|
||||
if (server >= parsePercent.value) {
|
||||
parsePercent.value = server
|
||||
if (data.message) parseMessage.value = data.message
|
||||
}
|
||||
if (data.status === 'FAIL') parseStatus.value = 'exception'
|
||||
} catch {
|
||||
// 进度查询失败时继续等预览结果
|
||||
}
|
||||
}, 400)
|
||||
}
|
||||
|
||||
function stopProgress() {
|
||||
if (progressTimer) {
|
||||
window.clearInterval(progressTimer)
|
||||
progressTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +342,14 @@ defineExpose({ open })
|
||||
.import-form {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.parse-progress {
|
||||
margin-top: 14px;
|
||||
}
|
||||
.parse-progress-msg {
|
||||
margin-top: 8px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
}
|
||||
.sheet-scroll {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Upload" @click="importRef.open()" v-hasPermi="['price:pay:import']">导入价卡</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Setting" :disabled="!selection.length" @click="feeRuleRef.open(selection)" v-hasPermi="['price:pay:edit']">附加费配置</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="Delete" :disabled="!selection.length" @click="handleDelete(ids)" v-hasPermi="['price:pay:remove']">删除</el-button>
|
||||
</el-col>
|
||||
@@ -63,7 +60,6 @@
|
||||
</basic-table>
|
||||
|
||||
<pay-import-dialog ref="importRef" @saved="tableRef.refresh()" />
|
||||
<pay-fee-rule-dialog ref="feeRuleRef" />
|
||||
|
||||
<el-drawer v-model="historyVisible" :title="historyTitle" size="980px" destroy-on-close>
|
||||
<div class="history-bar">
|
||||
@@ -154,12 +150,10 @@
|
||||
import { listPay, getPay, delPay, updatePay, listPayTerm, updatePayTerm, changePayTermStatus, comparePay } from '@/api/price/pay'
|
||||
import PayImportDialog from './components/PayImportDialog.vue'
|
||||
import PayConfigDetail from './components/PayConfigDetail.vue'
|
||||
import PayFeeRuleDialog from './components/PayFeeRuleDialog.vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const importRef = ref()
|
||||
const feeRuleRef = ref()
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const editing = ref(false)
|
||||
|
||||
Reference in New Issue
Block a user