Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 039c0d4858 | |||
| 43299e5329 | |||
| 3fe34c471c | |||
| 134855fc31 | |||
| 01cee41301 | |||
| 1f0d757e29 | |||
| 762cad2c0d | |||
| 7ef3f40a4f | |||
| 58e41b669d | |||
| 9a48abbacd | |||
| f95edbdf0a | |||
| b30dfdf675 | |||
| 9a393fe7dc | |||
| a2468099c2 | |||
| cbed4dc2a3 | |||
| d66b96b2ff | |||
| cdfc90556d | |||
| dfa90d6e33 | |||
| 4a34075022 | |||
| 6a03300f2f | |||
| 7496f8cbc6 | |||
| 2f2305825a | |||
| b79cd6f995 | |||
| 70a3c34c20 | |||
| 2b5633190e | |||
| 77dfb4291b | |||
| d71b7f04fe | |||
| f8ae347846 | |||
| 94991a1ed3 |
@@ -31,11 +31,9 @@ import com.bms.ar.domain.ArBillGenerateTask;
|
||||
import com.bms.ar.domain.ArBillLog;
|
||||
import com.bms.ar.domain.vo.ArBillActionRequest;
|
||||
import com.bms.ar.domain.vo.ArBillGenerateQuery;
|
||||
import com.bms.ar.domain.vo.ArBillGeneratePreviewExportRow;
|
||||
import com.bms.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.bms.ar.domain.vo.ArBillListExportRow;
|
||||
import com.bms.ar.domain.vo.ArBillQuery;
|
||||
import com.bms.ar.enums.ArBillDateType;
|
||||
import com.bms.ar.service.IArBillService;
|
||||
import com.bms.ar.security.ArBillScopeResolver;
|
||||
import com.bms.common.annotation.Log;
|
||||
@@ -175,28 +173,6 @@ public class ArBillController extends BaseController {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 按生成预览使用的相同筛选条件导出全部匹配订单,不受分页参数影响。 */
|
||||
@Log(title = "应收账单生成订单", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||
@PostMapping("/generate/preview/export")
|
||||
public void exportGeneratePreview(HttpServletResponse response, ArBillGenerateQuery query,
|
||||
HttpServletRequest request) {
|
||||
rejectMultiValueParameter(request, "customerId");
|
||||
ArBillGenerateQuery exportQuery = query == null ? new ArBillGenerateQuery() : query;
|
||||
List<PrcOrderBill> bills = billService.previewBill(exportQuery);
|
||||
List<ArBillGeneratePreviewExportRow> rows = new ArrayList<>(bills.size());
|
||||
for (PrcOrderBill bill : bills) {
|
||||
rows.add(ArBillGeneratePreviewExportRow.from(bill));
|
||||
}
|
||||
ExcelUtil<ArBillGeneratePreviewExportRow> util = new ExcelUtil<>(ArBillGeneratePreviewExportRow.class);
|
||||
if (ArBillDateType.PICKUP_DATE == ArBillDateType.of(exportQuery.getDateType())) {
|
||||
util.hideColumn("bizDate");
|
||||
} else {
|
||||
util.hideColumn("pickupDate");
|
||||
}
|
||||
util.exportExcel(response, rows, "应收账单待生成订单");
|
||||
}
|
||||
|
||||
/** 查询需要从全选结果中排除的订单;空条件不执行查询,避免返回全部订单。 */
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||
@PostMapping("/generate/exclusion-preview")
|
||||
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
package com.bms.web.controller.data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
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 org.springframework.web.multipart.MultipartFile;
|
||||
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.exception.ServiceException;
|
||||
import com.bms.price.domain.PayPostcodeGroup;
|
||||
import com.bms.price.domain.PayPostcodeLine;
|
||||
import com.bms.price.service.IPayPostcodeService;
|
||||
|
||||
/**
|
||||
* 数据管理下的应付邮编配置。Excel 导入后按国家和附加费金额合并。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/data/payPostcode")
|
||||
public class PayPostcodeController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IPayPostcodeService payPostcodeService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('data:payPostcode:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PayPostcodeGroup query)
|
||||
{
|
||||
startPage();
|
||||
List<PayPostcodeGroup> list = payPostcodeService.selectGroupList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('data:payPostcode:query')")
|
||||
@GetMapping("/{groupId}")
|
||||
public AjaxResult getInfo(@PathVariable Long groupId)
|
||||
{
|
||||
return success(payPostcodeService.selectGroupById(groupId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('data:payPostcode:edit')")
|
||||
@Log(title = "应付邮编配置", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody PayPostcodeGroup group)
|
||||
{
|
||||
return toAjax(payPostcodeService.updateGroup(group));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('data:payPostcode:query')")
|
||||
@GetMapping("/line")
|
||||
public TableDataInfo lines(PayPostcodeLine query)
|
||||
{
|
||||
startPage();
|
||||
List<PayPostcodeLine> list = payPostcodeService.selectLineList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('data:payPostcode:import')")
|
||||
@Log(title = "应付邮编配置", businessType = BusinessType.IMPORT)
|
||||
@PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public AjaxResult importExcel(MultipartFile file) throws Exception
|
||||
{
|
||||
if (file == null || file.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择 Excel 文件");
|
||||
}
|
||||
String name = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase(Locale.ROOT);
|
||||
if (!name.endsWith(".xlsx"))
|
||||
{
|
||||
throw new ServiceException("只支持 xlsx 文件");
|
||||
}
|
||||
return success(payPostcodeService.importExcel(file.getBytes(), file.getOriginalFilename(), getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('data:payPostcode:remove')")
|
||||
@Log(title = "应付邮编配置", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{groupIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] groupIds)
|
||||
{
|
||||
return toAjax(payPostcodeService.deleteByIds(groupIds));
|
||||
}
|
||||
}
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
+2
-31
@@ -16,7 +16,6 @@ import com.bms.common.core.page.TableSupport;
|
||||
import com.bms.price.search.BillSearchPage;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.search.PriceChangeGroupPage;
|
||||
|
||||
/**
|
||||
* 来自 ES 的价格变更前后日志(销售价 / 产品价 / 红线价)。
|
||||
@@ -32,40 +31,12 @@ public class PriceLogController extends BaseController
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(
|
||||
@RequestParam(value = "bizId", required = false) String bizId,
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "psvId", required = false) Long psvId,
|
||||
@RequestParam(value = "priceKind", required = false) String priceKind,
|
||||
@RequestParam(value = "productCode", required = false) String productCode,
|
||||
@RequestParam(value = "updateTime", required = false) String updateTime,
|
||||
@RequestParam(value = "updateTimePrefix", required = false) String updateTimePrefix,
|
||||
@RequestParam(value = "countryKeyword", required = false) String countryKeyword)
|
||||
@RequestParam(value = "status", required = false) String status)
|
||||
{
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
|
||||
BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, psvId, priceKind,
|
||||
productCode, updateTime, updateTimePrefix, countryKeyword, pageNum, pageSize);
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("查询成功");
|
||||
rsp.setRows(page.getRows());
|
||||
rsp.setTotal(page.getTotal());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
/** Group by productCode + write time for package-version drill-down. */
|
||||
@PreAuthorize("@ss.hasPermi('monitor:priceLog:list')")
|
||||
@GetMapping("/groups")
|
||||
public TableDataInfo groups(
|
||||
@RequestParam(value = "psvId", required = true) Long psvId,
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "priceKind", required = false) String priceKind)
|
||||
{
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
|
||||
PriceChangeGroupPage page = billSearchStore.searchPriceChangeGroups(psvId, priceKind, status, pageNum,
|
||||
pageSize);
|
||||
BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, pageNum, pageSize);
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("查询成功");
|
||||
|
||||
@@ -22,12 +22,9 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.constant.HttpStatus;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.PageDomain;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.core.page.TableSupport;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.MessageUtils;
|
||||
@@ -57,11 +54,6 @@ import com.bms.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.bms.price.enums.BillTaskSuccessStatus;
|
||||
import com.bms.price.enums.BillTaskType;
|
||||
import com.bms.price.search.BillSearchPage;
|
||||
import com.bms.price.search.BillSyncSnapshot;
|
||||
import com.bms.price.search.EsBizLogSupport;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.service.IPrcBillingTaskService;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
import com.bms.price.service.ITmsPriceQuoteService;
|
||||
@@ -92,9 +84,6 @@ public class BillingController extends BaseController
|
||||
@Autowired
|
||||
private OrderBillProductCheckService orderBillProductCheckService;
|
||||
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
/**
|
||||
* Historical quote list.
|
||||
*/
|
||||
@@ -316,14 +305,6 @@ public class BillingController extends BaseController
|
||||
return success(task);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:orderBill:rebill')")
|
||||
@Log(title = "Order bill local rebill cancel", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/orderBill/rebill/task/{taskId}/cancel")
|
||||
public AjaxResult cancelLocalRebill(@PathVariable("taskId") String taskId)
|
||||
{
|
||||
return success(prcOrderBillService.cancelLocalRebill(taskId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Order no options for remote-search dropdown (bill list multi search).
|
||||
*/
|
||||
@@ -366,97 +347,6 @@ public class BillingController extends BaseController
|
||||
return success(prcOrderBillService.getBill(billId));
|
||||
}
|
||||
|
||||
/**
|
||||
* ES BILL logs for this order (billing / upstream sync / fail snapshots).
|
||||
* bizId prefers transferNo then orderNo (same as write path).
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:bill') or @ss.hasPermi('price:orderBill:list')")
|
||||
@GetMapping("/bill/{billId}/history")
|
||||
public TableDataInfo billHistory(@PathVariable("billId") String billId)
|
||||
{
|
||||
if (StringUtils.isEmpty(billId))
|
||||
{
|
||||
return getDataTable(new ArrayList<BillSyncSnapshot>());
|
||||
}
|
||||
TmsPriceQuoteResult bill = prcOrderBillService.getBill(billId.trim());
|
||||
if (bill == null)
|
||||
{
|
||||
TableDataInfo empty = new TableDataInfo();
|
||||
empty.setCode(HttpStatus.SUCCESS);
|
||||
empty.setMsg("查询成功");
|
||||
empty.setRows(new ArrayList<BillSyncSnapshot>());
|
||||
empty.setTotal(0L);
|
||||
return empty;
|
||||
}
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
|
||||
String primary = EsBizLogSupport.billBizId(bill.getTransferNo(), bill.getOrderNo());
|
||||
BillSearchPage page = billSearchStore.search(EsLogType.BILL, primary, null, pageNum, pageSize);
|
||||
// Also pull by orderNo when transferNo was used as bizId, so older orderNo-keyed rows appear.
|
||||
if (StringUtils.isNotEmpty(bill.getTransferNo()) && StringUtils.isNotEmpty(bill.getOrderNo())
|
||||
&& !bill.getTransferNo().trim().equalsIgnoreCase(bill.getOrderNo().trim())
|
||||
&& pageNum == 1)
|
||||
{
|
||||
BillSearchPage byOrder = billSearchStore.search(EsLogType.BILL, bill.getOrderNo().trim(), null, 1,
|
||||
Math.max(pageSize, 50));
|
||||
page = mergeBillHistory(page, byOrder, pageSize);
|
||||
}
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("查询成功");
|
||||
rsp.setRows(page.getRows());
|
||||
rsp.setTotal(page.getTotal());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
private BillSearchPage mergeBillHistory(BillSearchPage primary, BillSearchPage secondary, int pageSize)
|
||||
{
|
||||
BillSearchPage out = new BillSearchPage();
|
||||
Map<String, BillSyncSnapshot> byId = new HashMap<String, BillSyncSnapshot>();
|
||||
if (primary != null && primary.getRows() != null)
|
||||
{
|
||||
for (BillSyncSnapshot row : primary.getRows())
|
||||
{
|
||||
if (row != null && StringUtils.isNotEmpty(row.getDocId()))
|
||||
{
|
||||
byId.put(row.getDocId(), row);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (secondary != null && secondary.getRows() != null)
|
||||
{
|
||||
for (BillSyncSnapshot row : secondary.getRows())
|
||||
{
|
||||
if (row != null && StringUtils.isNotEmpty(row.getDocId()) && !byId.containsKey(row.getDocId()))
|
||||
{
|
||||
byId.put(row.getDocId(), row);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<BillSyncSnapshot> merged = new ArrayList<BillSyncSnapshot>(byId.values());
|
||||
merged.sort((a, b) -> {
|
||||
String ta = a == null || a.getUpdateTime() == null ? "" : a.getUpdateTime();
|
||||
String tb = b == null || b.getUpdateTime() == null ? "" : b.getUpdateTime();
|
||||
return tb.compareTo(ta);
|
||||
});
|
||||
long total = Math.max(primary == null ? 0L : primary.getTotal(), merged.size());
|
||||
if (secondary != null)
|
||||
{
|
||||
total = Math.max(total, secondary.getTotal());
|
||||
}
|
||||
out.setTotal(total);
|
||||
if (merged.size() > pageSize)
|
||||
{
|
||||
out.setRows(new ArrayList<BillSyncSnapshot>(merged.subList(0, pageSize)));
|
||||
}
|
||||
else
|
||||
{
|
||||
out.setRows(merged);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refund one receivable bill via billing task billType=REFUND.
|
||||
*/
|
||||
|
||||
-453
@@ -1,453 +0,0 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.price.service.support.UploadFileBatch;
|
||||
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.MessageUtils;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.PrcCustomerPriceSet;
|
||||
import com.bms.price.domain.PrcCustomerPriceSetBatch;
|
||||
import com.bms.price.domain.PrcPriceExcelFile;
|
||||
import com.bms.price.domain.PrcPriceSet;
|
||||
import com.bms.price.domain.PrcPriceSetDetail;
|
||||
import com.bms.price.domain.PrcPriceSetSaveTask;
|
||||
import com.bms.price.domain.PrcPriceSetVersion;
|
||||
import com.bms.price.service.IPrcPriceExcelFileService;
|
||||
import com.bms.price.service.IPrcPriceSetService;
|
||||
|
||||
/**
|
||||
* Exclusive quote (shares prc_price_set with PACKAGE)
|
||||
*
|
||||
* @author bms
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/price/exclusive")
|
||||
public class ExclusiveQuoteController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IPrcPriceSetService priceSetService;
|
||||
|
||||
@Autowired
|
||||
private IPrcPriceExcelFileService priceExcelFileService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PrcPriceSet set)
|
||||
{
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
startPage();
|
||||
List<PrcPriceSet> list = priceSetService.selectPrcPriceSetList(set);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:query')")
|
||||
@GetMapping("/{priceSetId:[0-9]+}")
|
||||
public AjaxResult getInfo(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
return success(priceSetService.selectPrcPriceSetById(priceSetId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:add')")
|
||||
@Log(title = "Exclusive quote", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody PrcPriceSet set)
|
||||
{
|
||||
if (set.getPriceSetId() != null)
|
||||
{
|
||||
priceSetService.assertSetCategory(set.getPriceSetId(), PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
}
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
set.setCreateBy(getUsername());
|
||||
set.setUpdateBy(getUsername());
|
||||
String taskId = priceSetService.submitSaveAsync(set);
|
||||
// Do not pass bare String to success(): overload puts it into msg, not data.
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("taskId", taskId);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:query')")
|
||||
@GetMapping("/save/task/{taskId}")
|
||||
public AjaxResult getSaveTask(@PathVariable String taskId)
|
||||
{
|
||||
PrcPriceSetSaveTask task = priceSetService.getSaveTask(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
return error(MessageUtils.message("price.set.save.task.notExists"));
|
||||
}
|
||||
return success(task);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit')")
|
||||
@Log(title = "Exclusive quote", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@Validated @RequestBody PrcPriceSet set)
|
||||
{
|
||||
priceSetService.assertSetCategory(set.getPriceSetId(), PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
set.setUpdateBy(getUsername());
|
||||
return toAjax(priceSetService.updatePrcPriceSet(set));
|
||||
}
|
||||
|
||||
/** Update package name and note only. */
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit')")
|
||||
@Log(title = "Exclusive quote basic", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/basic")
|
||||
public AjaxResult editBasic(@RequestBody PrcPriceSet set)
|
||||
{
|
||||
priceSetService.assertSetCategory(set.getPriceSetId(), PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
set.setUpdateBy(getUsername());
|
||||
return toAjax(priceSetService.updateBasicInfo(set));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:remove')")
|
||||
@Log(title = "Exclusive quote", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{priceSetIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] priceSetIds)
|
||||
{
|
||||
if (priceSetIds != null)
|
||||
{
|
||||
for (Long id : priceSetIds)
|
||||
{
|
||||
priceSetService.assertSetCategory(id, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
return toAjax(priceSetService.deletePrcPriceSetByIds(priceSetIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:audit')")
|
||||
@Log(title = "Exclusive quote提交审核", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/submit/{priceSetId}")
|
||||
public AjaxResult submit(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
return toAjax(priceSetService.submit(priceSetId, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:audit')")
|
||||
@Log(title = "Exclusive quote生效", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/approve/{priceSetId}")
|
||||
public AjaxResult approve(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
return toAjax(priceSetService.approve(priceSetId, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:audit')")
|
||||
@Log(title = "Exclusive quote驳回", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/reject/{priceSetId}")
|
||||
public AjaxResult reject(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
return toAjax(priceSetService.reject(priceSetId, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit')")
|
||||
@Log(title = "Exclusive quote version activate", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/version/activate/{psvId}")
|
||||
public AjaxResult activateVersion(@PathVariable Long psvId, @RequestBody(required = false) PrcPriceSetVersion body)
|
||||
{
|
||||
Date effectiveTime = body == null ? null : body.getEffectiveTime();
|
||||
return toAjax(priceSetService.activateVersion(psvId, effectiveTime, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:add')")
|
||||
@Log(title = "Exclusive quote version copy", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/version/copy/{psvId}")
|
||||
public AjaxResult copyVersion(@PathVariable Long psvId)
|
||||
{
|
||||
Long newPsvId = priceSetService.copyVersion(psvId, getUsername());
|
||||
return success(newPsvId);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:add')")
|
||||
@Log(title = "Exclusive quote version create", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/version/create/{priceSetId}")
|
||||
public AjaxResult createVersion(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
Long newPsvId = priceSetService.createVersion(priceSetId, getUsername());
|
||||
return success(newPsvId);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit')")
|
||||
@Log(title = "Exclusive quote version delete", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/version/{psvId}")
|
||||
public AjaxResult deleteVersion(@PathVariable Long psvId)
|
||||
{
|
||||
return toAjax(priceSetService.deleteVersion(psvId, getUsername()));
|
||||
}
|
||||
|
||||
/** Import excel attachments for a version (prc_price_excel_file.psv_id). */
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:list')")
|
||||
@GetMapping("/version/{psvId}/excelFiles")
|
||||
public AjaxResult versionExcelFiles(@PathVariable Long psvId)
|
||||
{
|
||||
List<PrcPriceExcelFile> list = priceExcelFileService.selectByPsvId(psvId);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:audit')")
|
||||
@Log(title = "Exclusive quote disable", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/disable/{priceSetId}")
|
||||
public AjaxResult disable(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
return toAjax(priceSetService.disable(priceSetId, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:audit')")
|
||||
@Log(title = "Exclusive quote enable", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/enable/{priceSetId}")
|
||||
public AjaxResult enable(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
return toAjax(priceSetService.enable(priceSetId, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:add')")
|
||||
@Log(title = "Exclusive quote copy", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/copy/{priceSetId}")
|
||||
public AjaxResult copy(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
Long id = priceSetService.copy(priceSetId, getUsername());
|
||||
return success(id);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:query')")
|
||||
@PostMapping("/validate")
|
||||
public AjaxResult validate(@RequestBody PrcPriceSet set)
|
||||
{
|
||||
return success(priceSetService.validate(set));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:list')")
|
||||
@GetMapping("/detail/list")
|
||||
public TableDataInfo detailList(PrcPriceSetDetail detail)
|
||||
{
|
||||
startPage();
|
||||
List<PrcPriceSetDetail> list = priceSetService.selectDetailList(detail);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/** All detail rows for one product (no page); used by edit-price drawer. */
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:list')")
|
||||
@GetMapping("/detail/byProduct")
|
||||
public AjaxResult detailByProduct(@RequestParam("psvId") Long psvId,
|
||||
@RequestParam("productCode") String productCode)
|
||||
{
|
||||
return success(priceSetService.selectDetailListByProduct(psvId, productCode));
|
||||
}
|
||||
|
||||
/** All detail rows for a version (no page); batch prefetch instead of N byProduct calls. */
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:list')")
|
||||
@GetMapping("/detail/byVersion")
|
||||
public AjaxResult detailByVersion(@RequestParam("psvId") Long psvId)
|
||||
{
|
||||
return success(priceSetService.selectDetailListByVersion(psvId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:list')")
|
||||
@GetMapping("/detail/options")
|
||||
public AjaxResult detailOptions(@RequestParam("psvId") Long psvId)
|
||||
{
|
||||
return success(priceSetService.selectDetailOptions(psvId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit') or @ss.hasPermi('price:exclusive:forceEdit')")
|
||||
@Log(title = "Exclusive quote明细", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/detail")
|
||||
public AjaxResult addDetail(@RequestBody PrcPriceSetDetail detail)
|
||||
{
|
||||
return toAjax(priceSetService.insertDetail(detail));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit') or @ss.hasPermi('price:exclusive:forceEdit')")
|
||||
@Log(title = "Exclusive quote明细", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/detail")
|
||||
public AjaxResult editDetail(@RequestBody PrcPriceSetDetail detail)
|
||||
{
|
||||
return toAjax(priceSetService.updateDetail(detail));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit') or @ss.hasPermi('price:exclusive:forceEdit')")
|
||||
@Log(title = "Exclusive quote价格导入", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/detail/importPrices")
|
||||
public AjaxResult importDetailPrices(
|
||||
@RequestParam(value = "files", required = false) MultipartFile[] files,
|
||||
@RequestParam(value = "file", required = false) MultipartFile file,
|
||||
@RequestParam("priceSetId") Long priceSetId,
|
||||
@RequestParam(value = "psvId", required = false) Long psvId,
|
||||
@RequestParam(value = "updateOnly", required = false, defaultValue = "false") boolean updateOnly,
|
||||
@RequestParam(value = "replaceByProductCountry", required = false, defaultValue = "false") boolean replaceByProductCountry,
|
||||
@RequestParam(value = "countryCode", required = false) String countryCode)
|
||||
throws Exception
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
String taskId = priceSetService.submitImportDetailPricesAsync(priceSetId, psvId, UploadFileBatch.collect(files, file),
|
||||
getUsername(), updateOnly, replaceByProductCountry, countryCode);
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("taskId", taskId);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview pull product library (no DB write). For create wizard before finish.
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit') or @ss.hasPermi('price:exclusive:forceEdit') or @ss.hasPermi('price:exclusive:add')")
|
||||
@PostMapping("/detail/previewPullFromLibrary")
|
||||
public AjaxResult previewPullFromLibrary()
|
||||
{
|
||||
return success(priceSetService.previewPullFromLibrary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Excel import (no DB write). Optional detailsJson for updateOnly / build-price merge.
|
||||
* Optional countryCode = single-country dedicated-line Excel (same as product country import).
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit') or @ss.hasPermi('price:exclusive:forceEdit') or @ss.hasPermi('price:exclusive:add')")
|
||||
@PostMapping("/detail/previewImportPrices")
|
||||
public AjaxResult previewImportPrices(
|
||||
@RequestParam(value = "files", required = false) MultipartFile[] files,
|
||||
@RequestParam(value = "file", required = false) MultipartFile file,
|
||||
@RequestParam(value = "updateOnly", required = false, defaultValue = "false") boolean updateOnly,
|
||||
@RequestParam(value = "replaceByProductCountry", required = false, defaultValue = "false") boolean replaceByProductCountry,
|
||||
@RequestParam(value = "countryCode", required = false) String countryCode,
|
||||
@RequestParam(value = "detailsJson", required = false) String detailsJson) throws Exception
|
||||
{
|
||||
java.util.List<MultipartFile> batch = UploadFileBatch.collect(files, file);
|
||||
java.util.List<byte[]> bytesList = new java.util.ArrayList<byte[]>();
|
||||
java.util.List<String> names = new java.util.ArrayList<String>();
|
||||
for (MultipartFile item : batch)
|
||||
{
|
||||
bytesList.add(item.getBytes());
|
||||
names.add(item.getOriginalFilename());
|
||||
}
|
||||
List<PrcPriceSetDetail> existing = null;
|
||||
if (StringUtils.isNotEmpty(detailsJson))
|
||||
{
|
||||
existing = com.alibaba.fastjson2.JSON.parseArray(detailsJson, PrcPriceSetDetail.class);
|
||||
}
|
||||
return success(priceSetService.previewImportDetailPricesMany(
|
||||
bytesList, names, updateOnly, existing, replaceByProductCountry, countryCode, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Async pull product library into version details. Returns taskId; poll /save/task/{taskId}.
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit') or @ss.hasPermi('price:exclusive:forceEdit')")
|
||||
@Log(title = "Exclusive quote拉取产品", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/detail/pullFromLibrary")
|
||||
public AjaxResult pullFromLibrary(@RequestParam("priceSetId") Long priceSetId,
|
||||
@RequestParam(value = "psvId", required = false) Long psvId,
|
||||
@RequestParam(value = "force", required = false, defaultValue = "false") boolean force)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
String taskId = priceSetService.submitPullFromLibraryAsync(priceSetId, psvId, force, getUsername());
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("taskId", taskId);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async batch write of detail surcharge snapshots. Returns taskId; poll /save/task/{taskId}.
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit') or @ss.hasPermi('price:exclusive:forceEdit')")
|
||||
@Log(title = "Exclusive quote附加费", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/detail/surcharge/async")
|
||||
public AjaxResult batchSurchargeAsync(@RequestBody List<PrcPriceSetDetail> details)
|
||||
{
|
||||
String taskId = priceSetService.submitDetailSurchargeAsync(details);
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("taskId", taskId);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit') or @ss.hasPermi('price:exclusive:forceEdit')")
|
||||
@Log(title = "Exclusive quote明细", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/detail/{psdIds}")
|
||||
public AjaxResult removeDetail(@PathVariable Long[] psdIds)
|
||||
{
|
||||
return toAjax(priceSetService.deleteDetailByIds(psdIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:list')")
|
||||
@GetMapping("/customer/list")
|
||||
public TableDataInfo customerList(PrcCustomerPriceSet customer)
|
||||
{
|
||||
startPage();
|
||||
List<PrcCustomerPriceSet> list = priceSetService.selectCustomerList(customer);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit')")
|
||||
@Log(title = "Exclusive quote客户绑定", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/customer")
|
||||
public AjaxResult addCustomer(@RequestBody PrcCustomerPriceSet customer)
|
||||
{
|
||||
if (customer != null && customer.getPriceSetId() != null)
|
||||
{
|
||||
priceSetService.assertSetCategory(customer.getPriceSetId(), PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
}
|
||||
customer.setCreateBy(getUsername());
|
||||
return toAjax(priceSetService.insertCustomer(customer));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit')")
|
||||
@Log(title = "Exclusive quote客户批量绑定", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/customer/batch")
|
||||
public AjaxResult batchAddCustomer(@RequestBody PrcCustomerPriceSetBatch batch)
|
||||
{
|
||||
if (batch != null && batch.getPriceSetId() != null)
|
||||
{
|
||||
priceSetService.assertSetCategory(batch.getPriceSetId(), PrcPriceSet.CATEGORY_EXCLUSIVE);
|
||||
}
|
||||
batch.setCreateBy(getUsername());
|
||||
batch.setUpdateBy(getUsername());
|
||||
return success(priceSetService.batchInsertCustomers(batch));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit')")
|
||||
@Log(title = "Exclusive quote客户绑定", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/customer")
|
||||
public AjaxResult editCustomer(@RequestBody PrcCustomerPriceSet customer)
|
||||
{
|
||||
customer.setUpdateBy(getUsername());
|
||||
return toAjax(priceSetService.updateCustomer(customer));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:exclusive:edit')")
|
||||
@Log(title = "Exclusive quote客户绑定", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/customer/{customerPricesetIds}")
|
||||
public AjaxResult removeCustomer(@PathVariable Long[] customerPricesetIds)
|
||||
{
|
||||
return toAjax(priceSetService.deleteCustomerByIds(customerPricesetIds));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
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.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.price.domain.PayBill;
|
||||
import com.bms.price.domain.PayBillCommand;
|
||||
import com.bms.price.service.IPayBillService;
|
||||
|
||||
/**
|
||||
* 账单管理下的应付账单。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/price/payBill")
|
||||
public class PayBillController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IPayBillService payBillService;
|
||||
|
||||
/**
|
||||
* 分页查询应付账单。
|
||||
*
|
||||
* @param query 账单号、供应商、状态、币种,空着的条件不过滤
|
||||
* @return 账单列表,不含费用明细
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:payBill:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PayBill query)
|
||||
{
|
||||
startPage();
|
||||
List<PayBill> list = payBillService.selectPayBillList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一张应付账单的明细和操作日志。
|
||||
*
|
||||
* @param billId 应付账单ID
|
||||
* @return 账单头、费用行和日志
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:payBill:query')")
|
||||
@GetMapping("/{billId}")
|
||||
public AjaxResult detail(@PathVariable Long billId)
|
||||
{
|
||||
return success(payBillService.selectPayBillDetail(billId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览本次能生成账单的订单。只查询,不重算费用,也不写账单。
|
||||
*
|
||||
* @param command 查询方式,以及订单号、计费日或提单号
|
||||
* @return 可生成订单、未找到的单号、已经入账的订单
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:payBill:query')")
|
||||
@PostMapping("/preview")
|
||||
public AjaxResult preview(@RequestBody PayBillCommand command)
|
||||
{
|
||||
return success(payBillService.preview(command));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成应付账单。先重算每票订单的尾程应付,再把当时的费用抄进草稿账单。
|
||||
*
|
||||
* @param command 供应商,以及按订单号、计费日或提单号划定的范围
|
||||
* @return 刚生成的草稿账单
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:payBill:add')")
|
||||
@PostMapping
|
||||
public AjaxResult generate(@RequestBody PayBillCommand command)
|
||||
{
|
||||
return success(payBillService.generate(command));
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认草稿账单。确认后才可以登记付款。
|
||||
*
|
||||
* @param billId 应付账单ID
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:payBill:confirm')")
|
||||
@PutMapping("/confirm/{billId}")
|
||||
public AjaxResult confirm(@PathVariable Long billId)
|
||||
{
|
||||
payBillService.confirm(billId);
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 把已确认的账单登记为已付款。已付款的账单不能直接作废。
|
||||
*
|
||||
* @param billId 应付账单ID
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:payBill:confirm')")
|
||||
@PutMapping("/paid/{billId}")
|
||||
public AjaxResult paid(@PathVariable Long billId)
|
||||
{
|
||||
payBillService.markPaid(billId);
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 作废应付账单。账单和费用行保留,只改状态并记下作废原因。
|
||||
*
|
||||
* @param command 账单ID和作废原因
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:payBill:void')")
|
||||
@PutMapping("/void")
|
||||
public AjaxResult voidBill(@RequestBody PayBillCommand command)
|
||||
{
|
||||
payBillService.voidBill(command);
|
||||
return success();
|
||||
}
|
||||
}
|
||||
-249
@@ -1,249 +0,0 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
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.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.PayProduct;
|
||||
import com.bms.price.domain.PayProductTerm;
|
||||
import com.bms.price.domain.PaySaveCommand;
|
||||
import com.bms.price.pay.PayDetailExporter;
|
||||
import com.bms.price.service.IPayOrderCostService;
|
||||
import com.bms.price.service.IPayProductService;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 价格基础下的应付配置。导入先预览,确认后才入库。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/price/pay")
|
||||
public class PayProductController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IPayProductService payProductService;
|
||||
|
||||
@Autowired
|
||||
private IPayOrderCostService payOrderCostService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PayProduct query)
|
||||
{
|
||||
startPage();
|
||||
List<PayProduct> list = payProductService.selectPayProductList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/** 一个产品的价格版本和有效期 */
|
||||
@PreAuthorize("@ss.hasAnyPermi('price:pay:query,price:pay:edit')")
|
||||
@GetMapping("/term/list")
|
||||
public AjaxResult termList(@RequestParam Long productId)
|
||||
{
|
||||
return success(payProductService.listTerms(productId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:edit')")
|
||||
@Log(title = "应付有效期", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/term")
|
||||
public AjaxResult updateTerm(@RequestBody PayProductTerm term)
|
||||
{
|
||||
return toAjax(payProductService.updateTerm(term));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:edit')")
|
||||
@Log(title = "应付有效期", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/term/status")
|
||||
public AjaxResult changeTermStatus(@RequestBody PayProductTerm term)
|
||||
{
|
||||
return toAjax(payProductService.changeTermStatus(term));
|
||||
}
|
||||
|
||||
/** 按订单的渠道服务商和应付产品生成应付账单 */
|
||||
@PreAuthorize("@ss.hasAnyPermi('price:billing:bill,price:pay:edit')")
|
||||
@Log(title = "应付账单", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/order/{billId}")
|
||||
public AjaxResult generateOrderCost(@PathVariable String billId)
|
||||
{
|
||||
return success(payOrderCostService.generate(billId));
|
||||
}
|
||||
|
||||
/** 下载某一版导入时上传的源文件 */
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:query')")
|
||||
@PostMapping("/{productId}/source")
|
||||
public void downloadSource(@PathVariable Long productId, HttpServletResponse response) throws Exception
|
||||
{
|
||||
String filename = payProductService.sourceFileName(productId);
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setCharacterEncoding("utf-8");
|
||||
FileUtils.setAttachmentResponseHeader(response, filename);
|
||||
payProductService.writeSource(productId, response.getOutputStream());
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:query')")
|
||||
@GetMapping("/{productId}")
|
||||
public AjaxResult getInfo(@PathVariable Long productId)
|
||||
{
|
||||
return success(payProductService.selectPayProductDetail(productId));
|
||||
}
|
||||
|
||||
/** 下拉:已导入的公共模板,供渠道页选择。payType 为空时按应付 AP */
|
||||
@PreAuthorize("@ss.hasAnyPermi('price:pay:query,data:serviceChannel:list,data:serviceChannel:edit')")
|
||||
@GetMapping("/options")
|
||||
public AjaxResult options(@RequestParam(value = "payType", required = false) String payType)
|
||||
{
|
||||
return success(payProductService.selectTemplateOptions(payType));
|
||||
}
|
||||
|
||||
/** 渠道当前价格副本 */
|
||||
@PreAuthorize("@ss.hasAnyPermi('price:pay:query,data:serviceChannel:query,data:serviceChannel:edit')")
|
||||
@GetMapping("/channel/{channelId}")
|
||||
public AjaxResult channelPrice(@PathVariable String channelId)
|
||||
{
|
||||
return success(payProductService.selectChannelPrice(channelId));
|
||||
}
|
||||
|
||||
/** 渠道只绑定产品编号,以后按有效期取价 */
|
||||
@PreAuthorize("@ss.hasAnyPermi('price:pay:edit,data:serviceChannel:edit')")
|
||||
@Log(title = "渠道应付产品", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/channel/{channelId}/bind")
|
||||
public AjaxResult bindChannel(@PathVariable String channelId, @RequestBody ChannelProductMap link)
|
||||
{
|
||||
String payType = link == null ? null : link.getPayType();
|
||||
if (link == null || link.getProductId() == null)
|
||||
{
|
||||
payProductService.unbindChannel(channelId, payType);
|
||||
return success();
|
||||
}
|
||||
payProductService.bindChannel(channelId, link.getProductId(), link.getPriceSetId(), payType);
|
||||
return success(payProductService.selectChannelPrice(channelId));
|
||||
}
|
||||
|
||||
/** 对照两个价格版本 */
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:query')")
|
||||
@GetMapping("/compare")
|
||||
public AjaxResult compare(@RequestParam Long leftId, @RequestParam Long rightId)
|
||||
{
|
||||
return success(payProductService.compare(leftId, rightId));
|
||||
}
|
||||
|
||||
/** 给已绑定的产品追加一版,产品编号不变 */
|
||||
@PreAuthorize("@ss.hasAnyPermi('price:pay:import,data:serviceChannel:edit')")
|
||||
@Log(title = "渠道应付价格", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/channel/{channelId}")
|
||||
public AjaxResult saveChannel(@PathVariable String channelId, @RequestBody PayProduct product)
|
||||
{
|
||||
return success(payProductService.saveChannelVersion(channelId, product));
|
||||
}
|
||||
|
||||
/** 重新导入渠道价格,生成新版本,不改模板 */
|
||||
@PreAuthorize("@ss.hasAnyPermi('price:pay:import,data:serviceChannel:edit')")
|
||||
@Log(title = "渠道应付价格", businessType = BusinessType.IMPORT)
|
||||
@PostMapping(value = "/channel/{channelId}/reimport", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public AjaxResult reimportChannel(@PathVariable String channelId, MultipartFile file) throws Exception
|
||||
{
|
||||
assertWorkbook(file);
|
||||
return success(payProductService.reimportChannel(channelId, file.getBytes(), file.getOriginalFilename()));
|
||||
}
|
||||
|
||||
/** 按字典匹配工作表。dictValue 为空时读取文件里所有能对上的表 */
|
||||
@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,
|
||||
@RequestParam(value = "progressId", required = false) String progressId) throws Exception
|
||||
{
|
||||
assertWorkbook(file);
|
||||
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));
|
||||
}
|
||||
|
||||
/** 修改已导入的公共模板 */
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:import')")
|
||||
@Log(title = "应付配置", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody PayProduct product)
|
||||
{
|
||||
return toAjax(payProductService.updateDetail(product));
|
||||
}
|
||||
|
||||
/** 保存预览中勾选的产品 */
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:import')")
|
||||
@Log(title = "应付配置", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/save")
|
||||
public AjaxResult save(@RequestBody PaySaveCommand command)
|
||||
{
|
||||
return success(payProductService.save(command));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:remove')")
|
||||
@Log(title = "应付配置", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{productIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] productIds)
|
||||
{
|
||||
return toAjax(payProductService.deletePayProducts(productIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:export')")
|
||||
@Log(title = "应付配置", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, PayProduct query)
|
||||
{
|
||||
List<PayProduct> list = payProductService.selectPayProductList(query);
|
||||
new ExcelUtil<>(PayProduct.class).exportExcel(response, list, "应付产品");
|
||||
}
|
||||
|
||||
/** 按当前搜索条件导出产品、重量段、附加费和其他配置 */
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:export')")
|
||||
@Log(title = "应付配置明细", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/exportDetail")
|
||||
public void exportDetail(HttpServletResponse response, PayProduct query) throws Exception
|
||||
{
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setCharacterEncoding("utf-8");
|
||||
PayDetailExporter.write(response.getOutputStream(), payProductService.selectPayProductDetails(query));
|
||||
}
|
||||
|
||||
private void assertWorkbook(MultipartFile file)
|
||||
{
|
||||
if (file == null || file.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择价卡文件");
|
||||
}
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null || !filename.toLowerCase(Locale.ROOT).endsWith(".xlsx"))
|
||||
{
|
||||
throw new ServiceException("请上传xlsx价卡");
|
||||
}
|
||||
if (file.getSize() > 30L * 1024 * 1024)
|
||||
{
|
||||
throw new ServiceException("价卡文件不能超过30MB");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,6 @@ public class PriceSetController extends BaseController
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PrcPriceSet set)
|
||||
{
|
||||
// Ordinary package module: never mix exclusive quotes into this list.
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_PACKAGE);
|
||||
startPage();
|
||||
List<PrcPriceSet> list = priceSetService.selectPrcPriceSetList(set);
|
||||
return getDataTable(list);
|
||||
@@ -66,7 +64,6 @@ public class PriceSetController extends BaseController
|
||||
@GetMapping("/{priceSetId:[0-9]+}")
|
||||
public AjaxResult getInfo(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_PACKAGE);
|
||||
return success(priceSetService.selectPrcPriceSetById(priceSetId));
|
||||
}
|
||||
|
||||
@@ -75,11 +72,6 @@ public class PriceSetController extends BaseController
|
||||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody PrcPriceSet set)
|
||||
{
|
||||
if (set.getPriceSetId() != null)
|
||||
{
|
||||
priceSetService.assertSetCategory(set.getPriceSetId(), PrcPriceSet.CATEGORY_PACKAGE);
|
||||
}
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_PACKAGE);
|
||||
set.setCreateBy(getUsername());
|
||||
set.setUpdateBy(getUsername());
|
||||
String taskId = priceSetService.submitSaveAsync(set);
|
||||
@@ -106,8 +98,6 @@ public class PriceSetController extends BaseController
|
||||
@PutMapping
|
||||
public AjaxResult edit(@Validated @RequestBody PrcPriceSet set)
|
||||
{
|
||||
priceSetService.assertSetCategory(set.getPriceSetId(), PrcPriceSet.CATEGORY_PACKAGE);
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_PACKAGE);
|
||||
set.setUpdateBy(getUsername());
|
||||
return toAjax(priceSetService.updatePrcPriceSet(set));
|
||||
}
|
||||
@@ -118,7 +108,6 @@ public class PriceSetController extends BaseController
|
||||
@PutMapping("/basic")
|
||||
public AjaxResult editBasic(@RequestBody PrcPriceSet set)
|
||||
{
|
||||
priceSetService.assertSetCategory(set.getPriceSetId(), PrcPriceSet.CATEGORY_PACKAGE);
|
||||
set.setUpdateBy(getUsername());
|
||||
return toAjax(priceSetService.updateBasicInfo(set));
|
||||
}
|
||||
|
||||
-39
@@ -1,26 +1,18 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import 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.StringUtils;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
|
||||
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
|
||||
import com.bms.price.domain.vo.SrvBillAirQuery;
|
||||
import com.bms.price.domain.vo.SrvBillAirVo;
|
||||
import com.bms.price.service.ISrvBillAirFeeService;
|
||||
import com.bms.price.service.ISrvBillAirService;
|
||||
|
||||
/**
|
||||
@@ -33,9 +25,6 @@ public class SrvBillAirController extends BaseController
|
||||
@Autowired
|
||||
private ISrvBillAirService srvBillAirService;
|
||||
|
||||
@Autowired
|
||||
private ISrvBillAirFeeService srvBillAirFeeService;
|
||||
|
||||
/**
|
||||
* 提单列表
|
||||
*/
|
||||
@@ -66,32 +55,4 @@ public class SrvBillAirController extends BaseController
|
||||
}
|
||||
return success(vo);
|
||||
}
|
||||
|
||||
/** 一票提单下未删除的费用 */
|
||||
@PreAuthorize("@ss.hasPermi('price:airBill:fee')")
|
||||
@GetMapping("/fee/list")
|
||||
public AjaxResult feeList(@RequestParam("billCode") String billCode)
|
||||
{
|
||||
return success(srvBillAirFeeService.listFees(billCode));
|
||||
}
|
||||
|
||||
/** 提单费用的新增、修改、删除日志,按页返回 */
|
||||
@PreAuthorize("@ss.hasPermi('price:airBill:fee')")
|
||||
@GetMapping("/fee/log")
|
||||
public TableDataInfo feeLog(@RequestParam("billCode") String billCode)
|
||||
{
|
||||
List<SrvBillAirFeeLog> list = srvBillAirFeeService.listLogs(billCode);
|
||||
TableDataInfo table = getDataTable(list);
|
||||
table.setRows(new ArrayList<>(list));
|
||||
return table;
|
||||
}
|
||||
|
||||
/** 只保存本次新增、修改、删除的费用,并写入提单日志 */
|
||||
@PreAuthorize("@ss.hasPermi('price:airBill:fee')")
|
||||
@Log(title = "提单费用", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/fee/save")
|
||||
public AjaxResult saveFee(@RequestBody SrvBillAirFeeSave command)
|
||||
{
|
||||
return success(srvBillAirFeeService.save(command));
|
||||
}
|
||||
}
|
||||
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
package com.bms.web.controller.report;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.bms.ar.profit.ArCommissionRate;
|
||||
import com.bms.ar.profit.ArCommissionService;
|
||||
import com.bms.ar.profit.ArProfitQuery;
|
||||
import com.bms.ar.profit.ArProfitService;
|
||||
import com.bms.ar.profit.ArWriteoffRate;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import com.bms.ar.profit.ArProfitRow;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.common.utils.poi.ExcelUtil;
|
||||
import com.bms.price.enums.ProfitAreaRule;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/report/ar-profit")
|
||||
public class ArProfitController extends BaseController
|
||||
{
|
||||
private final ArProfitService report;
|
||||
private final ArCommissionService commissions;
|
||||
|
||||
public ArProfitController(ArProfitService report, ArCommissionService commissions)
|
||||
{
|
||||
this.report = report;
|
||||
this.commissions = commissions;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/summary")
|
||||
public AjaxResult summary(ArProfitQuery query) { return success(report.selectSummary(query)); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/sales-options")
|
||||
public AjaxResult salesOptions(@RequestParam(required = false) String keyword) { return success(report.selectSalesOptions(keyword)); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/area-rules")
|
||||
public AjaxResult areaRules() { return success(ProfitAreaRule.areaRules()); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/orders/{detailId}/fees")
|
||||
public AjaxResult fees(@PathVariable Long detailId) { return success(report.selectFees(detailId)); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/bills/{billId}/details")
|
||||
public TableDataInfo billDetails(@PathVariable Long billId)
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(report.selectBillDetails(billId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/bills")
|
||||
public TableDataInfo bills(ArProfitQuery query)
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(report.selectBills(query));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/rates")
|
||||
public AjaxResult rates() { return success(commissions.selectRates()); }
|
||||
|
||||
@Log(title = "账单毛利提成比例", businessType = BusinessType.INSERT)
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@PostMapping("/rates")
|
||||
public AjaxResult addRate(@RequestBody ArCommissionRate rate)
|
||||
{
|
||||
commissions.addRate(rate);
|
||||
return success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/writeoffs/missing-rates")
|
||||
public TableDataInfo missingRates()
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(commissions.missingRates());
|
||||
}
|
||||
|
||||
@Log(title = "历史核销提成比例确认", businessType = BusinessType.UPDATE)
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@PostMapping("/writeoffs/confirm-rate")
|
||||
public AjaxResult confirmRate(@RequestBody ArWriteoffRate input)
|
||||
{
|
||||
commissions.confirmMissingRate(input);
|
||||
return success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/commissions")
|
||||
public TableDataInfo commissions(@RequestParam(required = false) Long billId,
|
||||
@RequestParam(defaultValue = "false") boolean includeReversed)
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(commissions.results(billId, includeReversed));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/commissions/summary")
|
||||
public AjaxResult commissionSummary(@RequestParam(required = false) Long billId) { return success(commissions.summary(billId)); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/commissions/{billId}/writeoffs")
|
||||
public AjaxResult commissionWriteoffs(@PathVariable Long billId) { return success(commissions.writeoffRates(billId)); }
|
||||
|
||||
@Log(title = "应收账单提成明细", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@PostMapping("/commissions/export")
|
||||
public void exportCommissions(HttpServletResponse response, @RequestParam(required = false) Long billId)
|
||||
{
|
||||
new ExcelUtil<>(ArCommissionResult.class).exportExcel(response, commissions.results(billId, false), "应收账单提成明细");
|
||||
}
|
||||
|
||||
@Log(title = "应收账单毛利分析", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:export')")
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, ArProfitQuery query)
|
||||
{
|
||||
ExcelUtil<ArProfitRow> excel = new ExcelUtil<>(ArProfitRow.class);
|
||||
excel.exportExcel(response, report.selectBills(query), "应收账单毛利分析");
|
||||
}
|
||||
}
|
||||
+4
-29
@@ -16,7 +16,6 @@ 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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.config.BmsConfig;
|
||||
@@ -32,7 +31,6 @@ import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.common.utils.poi.ExcelUtil;
|
||||
// 提成功能暂停使用:import com.bms.price.domain.PrcProfitCommissionRate;
|
||||
import com.bms.price.domain.PrcProfitReportExport;
|
||||
import com.bms.price.enums.ProfitAreaRule;
|
||||
import com.bms.price.domain.vo.PrcProfitReportOrderRow;
|
||||
import com.bms.price.domain.vo.PrcProfitReportProductRow;
|
||||
import com.bms.price.domain.vo.PrcProfitReportQuery;
|
||||
@@ -50,23 +48,6 @@ public class PrcProfitReportController extends BaseController
|
||||
@Autowired
|
||||
private OssStorage ossStorage;
|
||||
|
||||
/** 客户筛选选项,沿用毛利报表的访问权限和查询范围。 */
|
||||
@PreAuthorize("@ss.hasPermi('report:profit:list')")
|
||||
@GetMapping("/customer-options")
|
||||
public TableDataInfo customerOptions(@RequestParam(required = false) String keywords)
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(profitReportService.selectCustomerOptions(keywords));
|
||||
}
|
||||
|
||||
/** 客户区域字典值与毛利规则的映射。 */
|
||||
@PreAuthorize("@ss.hasPermi('report:profit:list')")
|
||||
@GetMapping("/area-rules")
|
||||
public AjaxResult areaRules()
|
||||
{
|
||||
return success(profitReportService.selectAreaRules());
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:profit:list')")
|
||||
@GetMapping("/summary")
|
||||
public AjaxResult summary(PrcProfitReportQuery query)
|
||||
@@ -127,7 +108,7 @@ public class PrcProfitReportController extends BaseController
|
||||
try
|
||||
{
|
||||
List<PrcProfitReportOrderRow> rows = profitReportService.selectOrderRows(query);
|
||||
return finishExport(export, rows, PrcProfitReportOrderRow.class, "订单毛利报表", query.getProfitRule());
|
||||
return finishExport(export, rows, PrcProfitReportOrderRow.class, "订单毛利报表");
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
@@ -145,7 +126,7 @@ public class PrcProfitReportController extends BaseController
|
||||
try
|
||||
{
|
||||
List<PrcProfitReportProductRow> rows = profitReportService.selectProductRows(query);
|
||||
return finishExport(export, rows, PrcProfitReportProductRow.class, "产品毛利报表", query.getProfitRule());
|
||||
return finishExport(export, rows, PrcProfitReportProductRow.class, "产品毛利报表");
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
@@ -209,8 +190,7 @@ public class PrcProfitReportController extends BaseController
|
||||
return export;
|
||||
}
|
||||
|
||||
private <T> AjaxResult finishExport(PrcProfitReportExport export, List<T> rows, Class<T> rowType, String sheetName,
|
||||
String profitRule)
|
||||
private <T> AjaxResult finishExport(PrcProfitReportExport export, List<T> rows, Class<T> rowType, String sheetName)
|
||||
{
|
||||
Path temp = null;
|
||||
try
|
||||
@@ -218,12 +198,7 @@ public class PrcProfitReportController extends BaseController
|
||||
temp = Files.createTempFile("profit-report-", ".xlsx");
|
||||
try (OutputStream output = Files.newOutputStream(temp))
|
||||
{
|
||||
ExcelUtil<T> excelUtil = new ExcelUtil<>(rowType);
|
||||
if (ProfitAreaRule.GUANGZHOU.name().equals(profitRule))
|
||||
{
|
||||
excelUtil.hideColumn("costAmount", "redlineGrossProfit", "redlineGrossMarginRate");
|
||||
}
|
||||
excelUtil.exportExcelToStream(rows, sheetName, output);
|
||||
new ExcelUtil<T>(rowType).exportExcelToStream(rows, sheetName, output);
|
||||
}
|
||||
String objectKey = "report/profit/" + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + ".xlsx";
|
||||
export.setStorageKey(ossStorage.upload(temp, objectKey,
|
||||
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
package com.bms.web.controller.system;
|
||||
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.constant.Constants;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.enums.UserStatus;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.framework.web.service.TokenService;
|
||||
import com.bms.framework.web.service.UserDetailsServiceImpl;
|
||||
import com.bms.system.service.ISysUserService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 管理员以目标用户身份排查问题。 */
|
||||
@RestController
|
||||
@RequestMapping("/system/user/impersonation")
|
||||
public class SysImpersonationController {
|
||||
private static final long MAX_DURATION_MILLIS = 30 * 60 * 1000L;
|
||||
|
||||
private final ISysUserService userService;
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
private final TokenService tokenService;
|
||||
|
||||
public SysImpersonationController(ISysUserService userService, UserDetailsServiceImpl userDetailsService,
|
||||
TokenService tokenService) {
|
||||
this.userService = userService;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('system:user:impersonate')")
|
||||
@Log(title = "用户代登录", businessType = BusinessType.GRANT, isSaveResponseData = false)
|
||||
@PostMapping("/{userId}")
|
||||
public AjaxResult start(@PathVariable Long userId) {
|
||||
LoginUser operator = SecurityUtils.getLoginUser();
|
||||
// 项目中超级管理员是 user_id=1;代登录不能嵌套,也不能用于进入管理员账号。
|
||||
if (!SecurityUtils.isAdmin() || operator.isImpersonating()) {
|
||||
throw new ServiceException("仅超级管理员可以代登录");
|
||||
}
|
||||
if (userId == null || SecurityUtils.isAdmin(userId) || userId.equals(operator.getUserId())) {
|
||||
throw new ServiceException("不能代登录管理员账号或当前账号");
|
||||
}
|
||||
userService.checkUserDataScope(userId);
|
||||
SysUser target = userService.selectUserById(userId);
|
||||
if (target == null || !UserStatus.OK.getCode().equals(target.getDelFlag())
|
||||
|| !UserStatus.OK.getCode().equals(target.getStatus())) {
|
||||
throw new ServiceException("目标用户不存在、已删除或已停用");
|
||||
}
|
||||
if (target.getRoles() != null && target.getRoles().stream().anyMatch(role -> role != null
|
||||
&& (role.isAdmin() || Constants.SUPER_ADMIN.equals(role.getRoleKey())))) {
|
||||
throw new ServiceException("不能代登录管理员角色账号");
|
||||
}
|
||||
LoginUser actingUser = (LoginUser) userDetailsService.createLoginUser(target);
|
||||
actingUser.setImpersonatorId(operator.getUserId());
|
||||
actingUser.setImpersonatorName(operator.getUsername());
|
||||
actingUser.setImpersonatorToken(operator.getToken());
|
||||
actingUser.setImpersonationExpireTime(System.currentTimeMillis() + MAX_DURATION_MILLIS);
|
||||
AjaxResult result = AjaxResult.success();
|
||||
result.put(Constants.TOKEN, tokenService.createToken(actingUser));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Log(title = "退出用户代登录", businessType = BusinessType.OTHER, isSaveResponseData = false)
|
||||
@PostMapping("/exit")
|
||||
public AjaxResult exit() {
|
||||
LoginUser user = SecurityUtils.getLoginUser();
|
||||
if (!user.isImpersonating()) {
|
||||
throw new ServiceException("当前不是代登录会话");
|
||||
}
|
||||
tokenService.delLoginUser(user.getToken());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,6 @@ public class SysLoginController
|
||||
ajax.put("user", user);
|
||||
ajax.put("roles", roles);
|
||||
ajax.put("permissions", permissions);
|
||||
ajax.put("impersonating", loginUser.isImpersonating());
|
||||
ajax.put("pwdChrtype", getSysAccountChrtype());
|
||||
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
|
||||
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
|
||||
|
||||
@@ -38,8 +38,8 @@ spring:
|
||||
initialSize: 5
|
||||
# 最小连接池数量
|
||||
minIdle: 10
|
||||
# 最大连接池数量(local rebill 16 workers + UI/MQ headroom)
|
||||
maxActive: 40
|
||||
# 最大连接池数量
|
||||
maxActive: 20
|
||||
# 配置获取连接等待超时的时间
|
||||
maxWait: 60000
|
||||
# 配置连接超时时间
|
||||
|
||||
@@ -51,8 +51,8 @@ spring:
|
||||
initialSize: 5
|
||||
# 最小连接池数量
|
||||
minIdle: 10
|
||||
# 最大连接池数量(local rebill 16 workers + UI/MQ headroom)
|
||||
maxActive: 40
|
||||
# 最大连接池数量
|
||||
maxActive: 20
|
||||
# 配置获取连接等待超时的时间
|
||||
maxWait: 60000
|
||||
# 配置连接超时时间
|
||||
|
||||
@@ -40,8 +40,6 @@ bms:
|
||||
# 同步已直接复用订单行(跳过每单 Oracle 公网查询),瓶颈在 MySQL 计费写入,
|
||||
# 同客户行锁冲突由 billWithDbRetry 3 次重试兜底,16 路实测吞吐最高。
|
||||
order-source-sync-threads: 16
|
||||
# Order-list local rebill workers (2~24). Pair with Druid maxActive >= threads + 8.
|
||||
local-rebill-threads: 16
|
||||
# Night auto-rebill: hours 0-8, pull 500 (all statuses) -> bill -> next 500 until empty / 09:00.
|
||||
# Empty page => no billing.
|
||||
night-retry:
|
||||
|
||||
@@ -224,11 +224,6 @@ price.bill.billingFail.weight=计费失败:销售价未匹配重量段
|
||||
price.bill.billingFail.catalogProduct=计费失败:产品价未匹配产品
|
||||
price.bill.billingFail.catalogCountry=计费失败:产品价未匹配国家
|
||||
price.bill.billingFail.catalogWeight=计费失败:产品价未匹配重量段
|
||||
price.bill.billingFail.packageNotEnabled=计费失败:套餐未启用
|
||||
price.bill.billingFail.noActivePackageVersion=计费失败:计费日无生效套餐版本
|
||||
price.bill.billingFail.noCustomerPackage=计费失败:客户未绑定套餐
|
||||
price.bill.packageNotEnabled=客户已绑定套餐但套餐未启用,无法计费
|
||||
price.quote.packageNotEnabled=客户已绑定套餐但套餐未启用,无法试算
|
||||
price.bill.export.filterRequired=请先设置列表查询条件后再导出
|
||||
price.bill.export.taskNotExists=导出任务不存在或已过期
|
||||
price.bill.export.tooMany=匹配订单超过上限 {0} 条,请缩小筛选范围
|
||||
@@ -237,6 +232,3 @@ price.bill.export.notReady=导出文件尚未生成或已失败
|
||||
price.bill.export.fileMissing=导出文件不存在或路径非法
|
||||
price.set.customer.batch.empty=请选择要绑定的客户
|
||||
price.set.customer.batch.done=批量绑定完成:共 {0} 个客户,新绑定 {1} 个,更换 {2} 个,跳过 {3} 个
|
||||
price.bill.recalc.taskCancelled=本地重新计费已取消
|
||||
price.bill.recalc.taskCancelNotRunning=任务已结束或不存在,无法取消
|
||||
price.bill.recalc.taskIdRequired=任务ID不能为空
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
package com.bms.price.pay;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.price.domain.PayOrderCostResult;
|
||||
import com.bms.price.domain.PayPostcodeGroup;
|
||||
import com.bms.price.domain.PayPostcodeLine;
|
||||
import com.bms.price.domain.PayProduct;
|
||||
import com.bms.price.domain.PaySurcharge;
|
||||
import com.bms.price.domain.PayWeightBand;
|
||||
|
||||
class PayCostCalculatorTest
|
||||
{
|
||||
@Test
|
||||
void royalMailChargesTicketPriceAndMatchingSurcharges()
|
||||
{
|
||||
PayOrderCostResult result = PayCostCalculator.calculate(royalMail(), new BigDecimal("4"),
|
||||
new BigDecimal("50"), new BigDecimal("40"), new BigDecimal("30"), "IV1 1AA", null, 1);
|
||||
assertEquals(new BigDecimal("3.42"), result.getPayTotal());
|
||||
assertEquals("GBP", result.getCurrency());
|
||||
assertTrue(hasFee(result, "基础运费", "1.62"));
|
||||
assertTrue(hasFee(result, "偏远地区附加费", "1.80"));
|
||||
assertTrue(result.getFees().stream().noneMatch(fee -> fee.getFeeName().contains("超大")));
|
||||
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()
|
||||
{
|
||||
PayOrderCostResult result = PayCostCalculator.calculate(royalMail(), new BigDecimal("21"),
|
||||
null, null, null, "SW1A 1AA", null, 1);
|
||||
assertTrue(result.getFees().isEmpty());
|
||||
assertTrue(result.getMessage().contains("超出"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void oversizeUsesTheHigherPercentWhenBothTiersMatch()
|
||||
{
|
||||
PayOrderCostResult result = PayCostCalculator.calculate(spainOneZone(), new BigDecimal("1"),
|
||||
new BigDecimal("130"), new BigDecimal("50"), new BigDecimal("40"), "28001", null, 1);
|
||||
assertTrue(hasFee(result, "基础运费", "2.27"));
|
||||
assertTrue(hasFee(result, "特大包裹收费", "1.59"));
|
||||
assertEquals(1, result.getFees().stream().filter(fee -> "特大包裹收费".equals(fee.getFeeName())).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void postcodeOutsideTheRemoteListUsesMainland()
|
||||
{
|
||||
PayProduct product = evri();
|
||||
PayOrderCostResult result = PayCostCalculator.calculate(product, new BigDecimal("0.522"),
|
||||
new BigDecimal("30.1"), new BigDecimal("24"), new BigDecimal("15.1"), "LL22 8PJ",
|
||||
"UK Mainland\n英国大陆", null, 1);
|
||||
assertEquals(new BigDecimal("1.57"), result.getPayTotal());
|
||||
assertTrue(hasFee(result, "基础运费", "1.57"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void remoteZoneUsesMainlandFreightAndItsOwnSurcharge()
|
||||
{
|
||||
PayProduct product = new PayProduct();
|
||||
product.setProductName("Evri 48H");
|
||||
product.setCurrency("GBP");
|
||||
product.setWeightBands(new ArrayList<>(List.of(
|
||||
band("0", "1", "1.51", "UK Mainland"),
|
||||
band("1", "2", "1.72", "UK Mainland"))));
|
||||
PaySurcharge light = fee(1L, "偏远附加费", "5.95", "GBP", "ZONE", null);
|
||||
light.setZoneName("Highlands& Islands");
|
||||
light.setWeightMin(BigDecimal.ZERO);
|
||||
light.setWeightMax(BigDecimal.ONE);
|
||||
PaySurcharge heavy = fee(2L, "偏远附加费", "6.20", "GBP", "ZONE", null);
|
||||
heavy.setZoneName("Highlands& Islands");
|
||||
heavy.setWeightMin(BigDecimal.ONE);
|
||||
heavy.setWeightMax(new BigDecimal("2"));
|
||||
product.setSurcharges(List.of(light, heavy));
|
||||
|
||||
PayOrderCostResult remote = PayCostCalculator.calculate(product, new BigDecimal("0.5"),
|
||||
null, null, null, "IV1 1AA", "Highlands& Islands", null, 1);
|
||||
assertTrue(hasFee(remote, "基础运费", "1.51"));
|
||||
assertTrue(hasFee(remote, "偏远附加费", "5.95"));
|
||||
assertEquals(1, remote.getFees().stream().filter(fee -> "偏远附加费".equals(fee.getFeeName())).count());
|
||||
|
||||
PayOrderCostResult mainland = PayCostCalculator.calculate(product, new BigDecimal("0.5"),
|
||||
null, null, null, "LL22 8PJ", "UK Mainland", null, 1);
|
||||
assertTrue(hasFee(mainland, "基础运费", "1.51"));
|
||||
assertTrue(mainland.getFees().stream().noneMatch(fee -> "偏远附加费".equals(fee.getFeeName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void spanishPrefixChargesFirstWeightThenExtraKg()
|
||||
{
|
||||
PayProduct product = new PayProduct();
|
||||
product.setProductName("CTT 48H");
|
||||
product.setCurrency("EUR");
|
||||
product.setWeightBands(new ArrayList<>(List.of(band("0", "5", "2.05", "马德里"))));
|
||||
PaySurcharge first = fee(1L, "偏远附加费", "7.1", "EUR", "POSTCODE", "07");
|
||||
first.setZoneName("Balearic Islands");
|
||||
first.setPostcodeParam("07");
|
||||
first.setWeightMin(BigDecimal.ZERO);
|
||||
first.setWeightMax(BigDecimal.ONE);
|
||||
PaySurcharge extra = fee(2L, "偏远附加费", "4", "EUR", "EXTRA_KG", "1");
|
||||
extra.setZoneName("Balearic Islands");
|
||||
extra.setPostcodeParam("07");
|
||||
extra.setPriceUnit("EUR/KG");
|
||||
product.setSurcharges(List.of(first, extra));
|
||||
|
||||
PayOrderCostResult light = PayCostCalculator.calculate(product, new BigDecimal("0.8"),
|
||||
null, null, null, "07008", "马德里", null, 1);
|
||||
assertTrue(hasFee(light, "基础运费", "2.05"));
|
||||
assertTrue(hasFee(light, "偏远附加费", "7.1"));
|
||||
assertEquals(1, light.getFees().stream().filter(fee -> "偏远附加费".equals(fee.getFeeName())).count());
|
||||
|
||||
PayOrderCostResult heavy = PayCostCalculator.calculate(product, new BigDecimal("3"),
|
||||
null, null, null, "07008", "马德里", null, 1);
|
||||
assertTrue(hasFee(heavy, "基础运费", "2.05"));
|
||||
assertTrue(hasFee(heavy, "偏远附加费", "7.1"));
|
||||
assertTrue(hasFee(heavy, "偏远附加费", "8"));
|
||||
|
||||
PayOrderCostResult mainland = PayCostCalculator.calculate(product, new BigDecimal("3"),
|
||||
null, null, null, "28001", "马德里", null, 1);
|
||||
assertTrue(mainland.getFees().stream().noneMatch(fee -> "偏远附加费".equals(fee.getFeeName())));
|
||||
assertTrue(PaySurchargeRules.matchesPostcode("07,35,38,51,52,9", "35001"));
|
||||
assertTrue(PaySurchargeRules.matchesPostcode("07,35,38,51,52,9", "9000"));
|
||||
assertFalse(PaySurchargeRules.matchesPostcode("07,35,38,51,52,9", "28001"));
|
||||
assertTrue(PaySurchargeRules.matchesPostcode("IV,HS,KA27-28", "KA27 8AA"));
|
||||
assertEquals("07", PaySurchargeRules.remotePrefixes("Balearic Islands 07邮编开头"));
|
||||
assertEquals("35,38", PaySurchargeRules.remotePrefixes("Canary Islands 35、38邮编开头"));
|
||||
assertEquals("9", PaySurchargeRules.remotePrefixes("Portuguese Island 9邮编开头"));
|
||||
assertEquals("", PaySurchargeRules.remotePrefixes("岛屿邮编地区"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void productPostcodeBookChoosesTheMatchingRemoteFee()
|
||||
{
|
||||
List<PayRemoteBook.Hit> hits = List.of(new PayRemoteBook.Hit("偏远附加费", new BigDecimal("1.80"), "", "Highlands"));
|
||||
PayOrderCostResult result = PayCostCalculator.calculate(royalMail(), new BigDecimal("4"),
|
||||
null, null, null, "IV1 1AA", null, null, 1, hits);
|
||||
assertTrue(hasFee(result, "偏远地区附加费", "1.80"));
|
||||
assertTrue(result.getFees().stream().noneMatch(fee -> "偏远地区附加费".equals(fee.getFeeName())
|
||||
&& fee.getAmount().compareTo(new BigDecimal("1.00")) == 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void linkedRemoteBookWithoutAHitDoesNotUseThePriceCardList()
|
||||
{
|
||||
PayOrderCostResult result = PayCostCalculator.calculate(royalMail(), new BigDecimal("4"),
|
||||
null, null, null, "IV1 1AA", null, null, 1, List.of());
|
||||
assertTrue(result.getFees().stream().noneMatch(fee -> fee.getFeeName().contains("偏远")));
|
||||
assertTrue(hasFee(result, "基础运费", "1.62"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void remoteBookChargesAManualIslandFeeAndPerKg()
|
||||
{
|
||||
PayProduct product = new PayProduct();
|
||||
product.setProductName("德国DPD挂号-国际件");
|
||||
product.setCurrency("EUR");
|
||||
product.setWeightBands(new ArrayList<>(List.of(band("0", "5", "3.20", ""))));
|
||||
PaySurcharge island = fee(31L, "岛屿附加费", "12", "EUR", "MANUAL", null);
|
||||
PaySurcharge perKg = fee(32L, "偏远附加费", "0.35", "EUR", "MANUAL", null);
|
||||
perKg.setPriceUnit("EUR/KG");
|
||||
product.setSurcharges(List.of(island, perKg));
|
||||
List<PayRemoteBook.Hit> hits = List.of(new PayRemoteBook.Hit("", null, "", "岛屿"));
|
||||
PayOrderCostResult result = PayCostCalculator.calculate(product, new BigDecimal("2"),
|
||||
null, null, null, "1234", null, null, 1, hits);
|
||||
assertTrue(hasFee(result, "岛屿附加费", "12"));
|
||||
assertTrue(hasFee(result, "偏远附加费", "0.70"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void remoteHitsFollowTheProductGroupAndCountry()
|
||||
{
|
||||
PayPostcodeGroup germany = group(44L, "西班牙GLS", "REMOTE", "", "DE");
|
||||
PayPostcodeGroup france = group(45L, "西班牙GLS", "REMOTE", "", "FR");
|
||||
PayPostcodeLine berlin = line(44L, "10115");
|
||||
PayPostcodeLine paris = line(45L, "75001");
|
||||
List<PayRemoteBook.Hit> de = PayRemoteBook.hits(List.of(germany, france), List.of(berlin, paris), "10115", "DE", "德国");
|
||||
assertEquals(1, de.size());
|
||||
List<PayRemoteBook.Hit> none = PayRemoteBook.hits(List.of(germany, france), List.of(berlin, paris), "28001", "ES", "西班牙");
|
||||
assertTrue(none.isEmpty());
|
||||
PayPostcodeGroup delivery = group(42L, "西班牙CTT", "", "", "ES");
|
||||
assertTrue(PayRemoteBook.hits(List.of(delivery), List.of(line(42L, "28001")), "28001", "ES", "西班牙") == null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void severalZonesCannotBePricedFromTheOrder()
|
||||
{
|
||||
PayProduct product = spainOneZone();
|
||||
PayWeightBand rural = band("0.01", "30", "3.12", "Rural Resto");
|
||||
product.getWeightBands().add(rural);
|
||||
PayOrderCostResult result = PayCostCalculator.calculate(product, new BigDecimal("1"),
|
||||
null, null, null, "28001", null, 1);
|
||||
assertTrue(result.getFees().isEmpty());
|
||||
assertTrue(result.getMessage().contains("多个地区"));
|
||||
}
|
||||
|
||||
private static boolean hasFee(PayOrderCostResult result, String name, String amount)
|
||||
{
|
||||
return result.getFees().stream().anyMatch(fee -> name.equals(fee.getFeeName())
|
||||
&& fee.getAmount().compareTo(new BigDecimal(amount)) == 0);
|
||||
}
|
||||
|
||||
private static PayProduct royalMail()
|
||||
{
|
||||
PayProduct product = new PayProduct();
|
||||
product.setProductName("英国RM");
|
||||
product.setWeightBands(List.of(
|
||||
band("0", "5", "1.62", ""),
|
||||
band("5.001", "10", "2.26", ""),
|
||||
band("10.001", "15", "3.51", ""),
|
||||
band("15.001", "20", "5.18", "")));
|
||||
product.getWeightBands().forEach(band -> {
|
||||
band.setChargeName("每票");
|
||||
band.setPriceUnit("GBP/票");
|
||||
});
|
||||
product.setSurcharges(List.of(
|
||||
fee(11L, "偏远地区附加费", "1.80", "GBP", "POSTCODE", "IV,HS,KA27-28"),
|
||||
fee(12L, "偏远地区附加费", "1.00", "GBP", "POSTCODE", "BT,TR21-25"),
|
||||
fee(13L, "签名费", "0.70", "GBP", "MANUAL", null),
|
||||
fee(14L, "超大附加费(包裹)", "12.50", "GBP", "SIZE", "box:61*46*46;volume:0.129"),
|
||||
fee(15L, "超重附加费(包裹)", "12.50", "GBP", "WEIGHT", "20")));
|
||||
return product;
|
||||
}
|
||||
|
||||
private static PayProduct spainOneZone()
|
||||
{
|
||||
PayProduct product = new PayProduct();
|
||||
product.setProductName("西邮-西班牙(主岛)");
|
||||
PayWeightBand band = band("0.01", "30", "2.27", "MAD-BCN Dense");
|
||||
band.setChargeName("每件");
|
||||
band.setPriceUnit("EUR/件");
|
||||
product.setWeightBands(new ArrayList<>(List.of(band)));
|
||||
product.setSurcharges(List.of(
|
||||
fee(21L, "特大包裹收费", "35", "%", "SIZE", "side:120;sum:210;join:or"),
|
||||
fee(22L, "特大包裹收费", "70", "%", "SIZE", "side:120;sum:210;join:and")));
|
||||
product.getSurcharges().get(0).setChargeText("运费另加35%");
|
||||
product.getSurcharges().get(1).setChargeText("运费另加70%");
|
||||
return product;
|
||||
}
|
||||
|
||||
private static PayProduct evri()
|
||||
{
|
||||
PayProduct product = new PayProduct();
|
||||
product.setProductName("英国evri");
|
||||
product.setCurrency("GBP");
|
||||
product.setWeightBands(new ArrayList<>(List.of(
|
||||
band("0", "2", "1.57", "UK Mainland\n英国大陆"),
|
||||
band("0", "2", "4.78", "Highlands& Islands\n高地和岛屿"))));
|
||||
product.getWeightBands().forEach(row -> row.setPriceUnit("GBP/ITEM"));
|
||||
return product;
|
||||
}
|
||||
|
||||
private static PayWeightBand band(String min, String max, String price, String zone)
|
||||
{
|
||||
PayWeightBand band = new PayWeightBand();
|
||||
band.setWeightMin(new BigDecimal(min));
|
||||
band.setWeightMax(new BigDecimal(max));
|
||||
band.setWeightLabel(min + "-" + max);
|
||||
band.setPrice(new BigDecimal(price));
|
||||
band.setZoneName(zone);
|
||||
return band;
|
||||
}
|
||||
|
||||
private static PayPostcodeGroup group(Long id, String quoteSheet, String coverage, String feeName, String country)
|
||||
{
|
||||
PayPostcodeGroup group = new PayPostcodeGroup();
|
||||
group.setGroupId(id);
|
||||
group.setQuoteSheet(quoteSheet);
|
||||
group.setCoverage(coverage);
|
||||
group.setFeeName(feeName);
|
||||
group.setCountryCode(country);
|
||||
group.setConfigKind("ZONE");
|
||||
return group;
|
||||
}
|
||||
|
||||
private static PayPostcodeLine line(Long groupId, String postcode)
|
||||
{
|
||||
PayPostcodeLine line = new PayPostcodeLine();
|
||||
line.setGroupId(groupId);
|
||||
line.setPostcode(postcode);
|
||||
return line;
|
||||
}
|
||||
|
||||
private static PaySurcharge fee(Long id, String name, String amount, String currency, String rule, String param)
|
||||
{
|
||||
PaySurcharge fee = new PaySurcharge();
|
||||
fee.setSurchargeId(id);
|
||||
fee.setFeeName(name);
|
||||
fee.setAmount(new BigDecimal(amount));
|
||||
fee.setCurrency(currency);
|
||||
fee.setChargeText(amount + " " + currency);
|
||||
fee.setCalcRule(rule);
|
||||
fee.setCalcParam(param);
|
||||
return fee;
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package com.bms.price.pay;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.price.domain.PayPostcodeGroup;
|
||||
|
||||
/**
|
||||
* 用 10 月价卡核对邮编星号、附加费合并和分区原文。
|
||||
*/
|
||||
class PayPostcodeImporterTest
|
||||
{
|
||||
private final PayPostcodeImporter importer = new PayPostcodeImporter();
|
||||
|
||||
@Test
|
||||
void stripsStarAndMergesSameFee() throws Exception
|
||||
{
|
||||
List<PayPostcodeGroup> groups = importer.parse(Files.readAllBytes(file("32、")), "32.xlsx");
|
||||
List<PayPostcodeGroup> fees = groups.stream().filter(item -> "FEE".equals(item.getConfigKind()) && "英国附加费".equals(item.getSheetName())).toList();
|
||||
assertEquals(4, fees.size(), fees.stream().map(item -> item.getFeeName() + "=" + item.getFeeText() + " x" + item.getLineCount()).toList().toString());
|
||||
PayPostcodeGroup remote195 = one(fees, "偏远附加费", "1.95GBP/票");
|
||||
PayPostcodeGroup remote115 = one(fees, "偏远附加费", "1.15GBP/票");
|
||||
PayPostcodeGroup evri = one(fees, "偏远附加费", "5.95GBP/票");
|
||||
PayPostcodeGroup air = one(fees, "空气减排附加费", "0.45GBP/票");
|
||||
assertEquals("Royal Mail", remote195.getChannelName());
|
||||
assertEquals("Royal Mail", remote115.getChannelName());
|
||||
assertEquals("EVRI", evri.getChannelName());
|
||||
assertEquals("EVRI", air.getChannelName());
|
||||
assertEquals(15, remote195.getLineCount());
|
||||
assertTrue(remote195.getLines().stream().anyMatch(line -> "PA60-78".equals(line.getPostcode())));
|
||||
assertTrue(remote195.getLines().stream().anyMatch(line -> "PA80".equals(line.getPostcode())));
|
||||
assertTrue(remote195.getLines().stream().anyMatch(line -> "KA27-28".equals(line.getPostcode())));
|
||||
assertEquals(6, remote115.getLineCount());
|
||||
assertTrue(remote115.getLines().stream().anyMatch(line -> "AB35-38".equals(line.getPostcode())));
|
||||
assertTrue(remote115.getLines().stream().anyMatch(line -> "AB53-56".equals(line.getPostcode())));
|
||||
assertTrue(evri.getLineCount() > 100 && evri.getLineCount() < 500);
|
||||
assertTrue(evri.getLines().stream().anyMatch(line -> "GY".equals(line.getPostcode()) && "GY*".equals(line.getPostcodeRaw())));
|
||||
assertTrue(air.getLineCount() > 70000);
|
||||
assertEquals("英国", air.getCountryName());
|
||||
assertEquals("REMOTE", remote195.getCoverage());
|
||||
assertEquals("REMOTE", remote115.getCoverage());
|
||||
assertEquals("REMOTE", evri.getCoverage());
|
||||
assertEquals("", air.getCoverage());
|
||||
assertTrue(groups.stream().noneMatch(item -> "Evri 48H".equals(item.getSheetName())));
|
||||
|
||||
PayPostcodeGroup italy = groups.stream().filter(item -> "iMile邮编分区".equals(item.getSheetName()) && "IT".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertEquals("ZONE", italy.getConfigKind());
|
||||
assertEquals("", italy.getCoverage());
|
||||
assertEquals("IT-iMile", italy.getChannelName());
|
||||
assertTrue(italy.getLines().stream().anyMatch(line -> "00010".equals(line.getPostcode()) && "province(no dense)".equals(line.getZoneName()) && "Lazio".equals(line.getRegionName()) && "ROMA".equals(line.getCityName())));
|
||||
assertTrue(groups.stream().anyMatch(item -> "iMile邮编分区".equals(item.getSheetName()) && "DE".equals(item.getCountryCode()) && "DE-iMile".equals(item.getChannelName()) && item.getLines().stream().anyMatch(line -> "01001".equals(line.getPostcode()))));
|
||||
PayPostcodeGroup spain = groups.stream().filter(item -> "CTT邮编分区".equals(item.getSheetName())).findFirst().orElseThrow();
|
||||
assertEquals("西班牙", spain.getCountryName());
|
||||
assertEquals("CTT", spain.getChannelName());
|
||||
assertTrue(spain.getLines().stream().anyMatch(line -> "01110".equals(line.getPostcode()) && "Low".equals(line.getZoneName()) && "SANTIKURUTZE KANPEZU".equals(line.getCityName())));
|
||||
PayPostcodeGroup france = groups.stream().filter(item -> "Colissimo邮编分区".equals(item.getSheetName())).findFirst().orElseThrow();
|
||||
assertEquals("Colissimo", france.getChannelName());
|
||||
assertTrue(france.getLines().stream().anyMatch(line -> "77244".equals(line.getPostcode()) && "Moyenne Densité".equals(line.getZoneName())));
|
||||
PayPostcodeGroup poste = groups.stream().filter(item -> "PosteItaliane邮编分区".equals(item.getSheetName()) && item.getLines().stream().anyMatch(line -> "City (dense)".equals(line.getZoneName()))).findFirst().orElseThrow();
|
||||
assertTrue(poste.getLines().stream().noneMatch(line -> "City".equals(line.getZoneName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripStarOnlyRemovesTrailingMark()
|
||||
{
|
||||
assertEquals("GY", PayPostcodeImporter.stripStar("GY*"));
|
||||
assertEquals("PO30", PayPostcodeImporter.stripStar("PO30*"));
|
||||
assertEquals("KA27-28", PayPostcodeImporter.stripStar("KA27-28"));
|
||||
assertEquals("Royal Mail", PayPostcodeImporter.channelOf(Arrays.asList("Royal Mail偏远地区附加费")));
|
||||
assertEquals("EVRI", PayPostcodeImporter.channelOf(Arrays.asList("EVRI偏远&空气减排地区附加费")));
|
||||
assertEquals("CTT", PayPostcodeImporter.channelOf(Arrays.asList("Código Postal", "CTT Density")));
|
||||
assertEquals("GOFO", PayPostcodeImporter.channelOf(Arrays.asList("法国GOFO可达邮编")));
|
||||
assertEquals("REACH", PayPostcodeImporter.coverageOf("法国GOFO可达邮编", "", ""));
|
||||
assertEquals("UNREACH", PayPostcodeImporter.coverageOf("德国DPD挂号(DEDPD)不可达邮编", "", ""));
|
||||
assertEquals("REMOTE", PayPostcodeImporter.coverageOf("evri 偏远", "", ""));
|
||||
assertEquals("", PayPostcodeImporter.coverageOf("英国附加费", "EVRI偏远&空气减排地区附加费", "空气减排附加费"));
|
||||
List<PayPostcodeImporter.PostcodeToken> kept = PayPostcodeImporter.expandPostcode("KA27-28");
|
||||
assertEquals("KA27-28", kept.get(0).postcode);
|
||||
assertEquals("", kept.get(0).end);
|
||||
List<PayPostcodeImporter.PostcodeToken> range = PayPostcodeImporter.expandPostcode("57030-57039");
|
||||
assertEquals("57030", range.get(0).postcode);
|
||||
assertEquals("57039", range.get(0).end);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reachableUnreachableAndRemoteKeepTheirOwnCountry() throws Exception
|
||||
{
|
||||
List<PayPostcodeGroup> may = importer.parse(Files.readAllBytes(file("20、")), "20.xlsx");
|
||||
PayPostcodeGroup cirro = may.stream().filter(item -> item.getSheetName().contains("Cirro可派")).findFirst().orElseThrow();
|
||||
assertEquals("REACH", cirro.getCoverage());
|
||||
assertEquals("法国", cirro.getCountryName());
|
||||
assertEquals("Cirro", cirro.getChannelName());
|
||||
assertTrue(cirro.getLines().stream().anyMatch(line -> "94350".equals(line.getPostcode()) && "Zone1".equals(line.getZoneName()) && "Paris".equals(line.getRegionName())));
|
||||
|
||||
PayPostcodeGroup germany = may.stream().filter(item -> "西班牙GLS 偏远邮编".equals(item.getSheetName()) && "DE".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertEquals("REMOTE", germany.getCoverage());
|
||||
assertEquals("德国", germany.getCountryName());
|
||||
assertEquals("GLS", germany.getChannelName());
|
||||
assertTrue(germany.getLines().stream().anyMatch(line -> "18565".equals(line.getPostcode()) && "Hiddensee".equals(line.getRegionName())));
|
||||
PayPostcodeGroup italyRemote = may.stream().filter(item -> "西班牙GLS 偏远邮编".equals(item.getSheetName()) && "IT".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertTrue(italyRemote.getLines().stream().anyMatch(line -> "57030".equals(line.getPostcode()) && "57039".equals(line.getPostcodeEnd())));
|
||||
|
||||
PayPostcodeGroup blocked = may.stream().filter(item -> item.getSheetName().contains("不可达")).findFirst().orElseThrow();
|
||||
assertEquals("UNREACH", blocked.getCoverage());
|
||||
assertEquals("德国", blocked.getCountryName());
|
||||
assertEquals("DPD", blocked.getChannelName());
|
||||
assertTrue(blocked.getLines().stream().anyMatch(line -> "18565".equals(line.getPostcode()) && "18565".equals(line.getPostcodeEnd())));
|
||||
|
||||
PayPostcodeGroup denmark = may.stream().filter(item -> item.getSheetName().contains("岛屿") && "DK".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertEquals("REMOTE", denmark.getCoverage());
|
||||
assertEquals("丹麦", denmark.getCountryName());
|
||||
assertEquals("DPD", denmark.getChannelName());
|
||||
assertTrue(denmark.getLines().stream().anyMatch(line -> "4592".equals(line.getPostcode())));
|
||||
PayPostcodeGroup estonia = may.stream().filter(item -> item.getSheetName().contains("岛屿") && "EE".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertEquals("爱沙尼亚", estonia.getCountryName());
|
||||
assertEquals("REMOTE", estonia.getCoverage());
|
||||
assertTrue(estonia.getLines().stream().anyMatch(line -> "88001".equals(line.getPostcode()) && "88005".equals(line.getPostcodeEnd())));
|
||||
|
||||
List<PayPostcodeGroup> yide = importer.parse(Files.readAllBytes(file("26、")), "26.xlsx");
|
||||
PayPostcodeGroup gofo = yide.stream().filter(item -> item.getSheetName().contains("GOFO可达")).findFirst().orElseThrow();
|
||||
assertEquals("REACH", gofo.getCoverage());
|
||||
assertEquals("法国", gofo.getCountryName());
|
||||
assertEquals("GOFO", gofo.getChannelName());
|
||||
assertTrue(gofo.getLines().stream().anyMatch(line -> "94350".equals(line.getPostcode()) && "Paris".equals(line.getRegionName())));
|
||||
|
||||
PayPostcodeGroup evriRemote = yide.stream().filter(item -> "evri 偏远".equals(item.getSheetName())).findFirst().orElseThrow();
|
||||
assertEquals("REMOTE", evriRemote.getCoverage());
|
||||
assertEquals("英国", evriRemote.getCountryName());
|
||||
assertEquals("evri", evriRemote.getChannelName());
|
||||
assertTrue(evriRemote.getLines().stream().anyMatch(line -> "GY".equals(line.getPostcode()) && "GY*".equals(line.getPostcodeRaw())));
|
||||
|
||||
PayPostcodeGroup peuItaly = yide.stream().filter(item -> item.getSheetName().contains("不可达") && "IT".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertEquals("UNREACH", peuItaly.getCoverage());
|
||||
assertEquals("意大利", peuItaly.getCountryName());
|
||||
assertTrue(peuItaly.getLines().stream().anyMatch(line -> "23041".equals(line.getPostcode())));
|
||||
List<PayPostcodeGroup> weishi = importer.parse(Files.readAllBytes(file("8、")), "8.xlsx");
|
||||
PayPostcodeGroup cttZone = weishi.stream().filter(item -> "西班牙CTT邮编分区".equals(item.getSheetName())).findFirst().orElseThrow();
|
||||
assertEquals("末端", cttZone.getServiceType());
|
||||
assertEquals("西班牙", cttZone.getCatalogCountry());
|
||||
assertEquals("CTT", cttZone.getProductName());
|
||||
assertEquals("西班牙CTT", cttZone.getQuoteSheet());
|
||||
assertEquals("", cttZone.getCoverage() == null ? "" : cttZone.getCoverage());
|
||||
assertTrue(cttZone.getLines().stream().anyMatch(line -> "01110".equals(line.getPostcode()) && "Low".equals(line.getZoneName()) && "ALAVA".equals(line.getRegionName())));
|
||||
PayPostcodeGroup cirroMay = weishi.stream().filter(item -> item.getSheetName().contains("Cirro可派")).findFirst().orElseThrow();
|
||||
assertEquals("末端", cirroMay.getServiceType());
|
||||
assertEquals("法国", cirroMay.getCatalogCountry());
|
||||
assertEquals("cirro", cirroMay.getProductName());
|
||||
assertEquals("法国Cirro", cirroMay.getQuoteSheet());
|
||||
assertEquals("REACH", cirroMay.getCoverage());
|
||||
PayPostcodeGroup glsMay = weishi.stream().filter(item -> "西班牙GLS 偏远邮编".equals(item.getSheetName()) && "DE".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertEquals("西班牙", glsMay.getCatalogCountry());
|
||||
assertEquals("GLS", glsMay.getProductName());
|
||||
assertEquals("西班牙GLS", glsMay.getQuoteSheet());
|
||||
assertEquals("德国", glsMay.getCountryName());
|
||||
assertEquals("REMOTE", glsMay.getCoverage());
|
||||
PayPostcodeGroup blockedMay = weishi.stream().filter(item -> item.getSheetName().contains("不可达")).findFirst().orElseThrow();
|
||||
assertEquals("德国", blockedMay.getCatalogCountry());
|
||||
assertEquals("DPD", blockedMay.getProductName());
|
||||
assertTrue(blockedMay.getQuoteSheet().contains("DEDPD"));
|
||||
assertEquals("UNREACH", blockedMay.getCoverage());
|
||||
PayPostcodeGroup islandMay = weishi.stream().filter(item -> item.getSheetName().contains("岛屿") && "DK".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertEquals("DPD国际件", islandMay.getProductName());
|
||||
assertEquals("德国DPD挂号-国际件", islandMay.getQuoteSheet());
|
||||
assertEquals("REMOTE", islandMay.getCoverage());
|
||||
assertTrue(weishi.stream().noneMatch(item -> item.getSheetName().contains("危机燃油") || item.getSheetName().contains("高风险")));
|
||||
|
||||
PayPostcodeGroup peuFrance = yide.stream().filter(item -> item.getSheetName().contains("不可达") && "FR".equals(item.getCountryCode())).findFirst().orElseThrow();
|
||||
assertTrue(peuFrance.getLines().stream().anyMatch(line -> "98700".equals(line.getPostcode()) && "98799".equals(line.getPostcodeEnd())));
|
||||
}
|
||||
|
||||
private static PayPostcodeGroup one(List<PayPostcodeGroup> fees, String name, String text)
|
||||
{
|
||||
return fees.stream().filter(item -> name.equals(item.getFeeName()) && text.equals(item.getFeeText())).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private Path file(String prefix) throws Exception
|
||||
{
|
||||
try (Stream<Path> files = Files.list(Path.of("D:/应付供应商")))
|
||||
{
|
||||
return files.filter(path -> path.getFileName().toString().startsWith(prefix) && path.toString().endsWith(".xlsx")).findFirst().orElseThrow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,768 +0,0 @@
|
||||
package com.bms.price.pay;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
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;
|
||||
import com.bms.price.domain.PayWeightBand;
|
||||
|
||||
/**
|
||||
* 用现有应付价卡核对目录产品和重量段、附加费是否被读出来。
|
||||
*/
|
||||
class PayWorkbookParserTest
|
||||
{
|
||||
private final PayWorkbookParser parser = new PayWorkbookParser();
|
||||
|
||||
@Test
|
||||
void octoberCatalogKeepsTwelveProductSheets() throws Exception
|
||||
{
|
||||
PayPreview preview = parse("9、");
|
||||
assertEquals(12, preview.getProducts().size());
|
||||
PayProduct evri = product(preview, "Evri 48H");
|
||||
assertTrue(evri.getWeightBands().stream().anyMatch(band -> decimal(band, "1.51") && "英国大陆".equals(zoneTail(band))));
|
||||
assertTrue(evri.getSurcharges().stream().anyMatch(fee -> "战争附加费".equals(fee.getFeeName()) && fee.getChargeText().contains("2.5")));
|
||||
PayProduct ctt = product(preview, "CTT 48H");
|
||||
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> band.getWeightLabel() != null && band.getWeightLabel().contains("首重") && decimal(band, "2.05")));
|
||||
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "每增加 1Kg".equals(band.getWeightLabel()) || (band.getWeightLabel() != null && band.getWeightLabel().contains("每增加"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mayCardReadsDhlAndDpdWithoutZoneSheets() throws Exception
|
||||
{
|
||||
PayPreview preview = parse("8、");
|
||||
assertEquals(8, preview.getProducts().size());
|
||||
PayProduct dhl = product(preview, "DHL泛欧");
|
||||
assertEquals("威狮", dhl.getSupplierName());
|
||||
assertTrue(dhl.getRelatedSheet() != null && dhl.getRelatedSheet().contains("高风险"));
|
||||
assertTrue(dhl.getWeightBands().stream().anyMatch(band -> decimal(band, "9.71") && band.getCountryName() != null && band.getCountryName().contains("丹麦")),
|
||||
dhl.getParseStatus() + " " + dhl.getSheetName() + " " + dhl.getWeightBands().stream().limit(6)
|
||||
.map(band -> band.getCountryName() + "/" + band.getChargeName() + "/" + band.getPrice()).toList());
|
||||
assertEquals("末端", dhl.getServiceType());
|
||||
assertEquals("德国", dhl.getCountryScope());
|
||||
assertEquals("DHL泛欧", dhl.getSheetName());
|
||||
PaySurcharge afghan = dhl.getSurcharges().stream()
|
||||
.filter(fee -> "高风险地区附加费".equals(fee.getFeeName()) && "AF".equals(fee.getCalcParam())).findFirst().orElseThrow();
|
||||
assertEquals("Afghanistan", afghan.getZoneName());
|
||||
assertEquals("RMB", afghan.getCurrency());
|
||||
assertEquals("RMB/KG", afghan.getPriceUnit());
|
||||
assertTrue(afghan.getChargeText().contains("10.4") && afghan.getChargeText().contains("RMB/kg"));
|
||||
assertTrue(decimalFee(afghan, "10.4"));
|
||||
assertEquals("MANUAL", afghan.getCalcRule());
|
||||
assertTrue(afghan.getFeeDesc().contains("非欧洲"));
|
||||
PayProduct dpd = preview.getProducts().stream().filter(item -> "DPD".equals(item.getProductName())).findFirst().orElseThrow();
|
||||
assertEquals("末端", dpd.getServiceType());
|
||||
assertEquals("德国", dpd.getCountryScope());
|
||||
assertTrue(dpd.getSheetName().contains("DEDPD"));
|
||||
assertTrue(dpd.getRelatedSheet().contains("不可达"));
|
||||
assertTrue(dpd.getRelatedSheet().contains("危机燃油"));
|
||||
assertTrue(dpd.getWeightBands().stream().anyMatch(band -> decimal(band, "3.29")));
|
||||
assertTrue(dpd.getSurcharges().stream().anyMatch(fee -> "道路通行费".equals(fee.getFeeName()) && decimalFee(fee, "0.14")));
|
||||
assertTrue(dpd.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("旺季") && fee.getEffectiveStart() != null));
|
||||
PaySurcharge bracket = dpd.getSurcharges().stream()
|
||||
.filter(fee -> "危机燃油附加费".equals(fee.getFeeName()) && "1.95|2".equals(fee.getCalcParam())).findFirst().orElseThrow();
|
||||
assertEquals("1.95 ≤ 油价 < 2", bracket.getChargeText());
|
||||
assertTrue(decimalFee(bracket, "0.0379"));
|
||||
assertEquals("MANUAL", bracket.getCalcRule());
|
||||
PaySurcharge zero = dpd.getSurcharges().stream()
|
||||
.filter(fee -> "危机燃油附加费".equals(fee.getFeeName()) && "|1.35".equals(fee.getCalcParam())).findFirst().orElseThrow();
|
||||
assertEquals("油价 < 1.35", zero.getChargeText());
|
||||
assertTrue(decimalFee(zero, "0"));
|
||||
PayProduct ctt = product(preview, "CTT");
|
||||
assertEquals("末端", ctt.getServiceType());
|
||||
assertEquals("西班牙", ctt.getCountryScope());
|
||||
assertEquals("西班牙CTT", ctt.getSheetName());
|
||||
assertTrue(ctt.getRelatedSheet().contains("西班牙CTT邮编分区"));
|
||||
PayProduct international = product(preview, "DPD国际件");
|
||||
assertEquals("德国", international.getCountryScope());
|
||||
assertTrue(international.getRelatedSheet().contains("岛屿"));
|
||||
assertTrue(international.getSurcharges().stream().noneMatch(fee -> "1.95|2".equals(fee.getCalcParam())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsZonesStayLeftToRightAndColissimoFuelIsMarked() throws Exception
|
||||
{
|
||||
PayPreview preview = parse("32、");
|
||||
PayProduct ups = product(preview, "德国UPS");
|
||||
String[] codes = {
|
||||
"DE",
|
||||
"BE", "LU", "NL", "AT", "SI",
|
||||
"CZ", "PL", "SK",
|
||||
"DK", "FI", "FR", "SE", "ES", "IT", "PT", "HU",
|
||||
"GR", "IE", "BG", "HR", "EE", "LT", "LV", "RO"
|
||||
};
|
||||
String[] zones = {
|
||||
"Zone 1",
|
||||
"Zone 2", "Zone 2", "Zone 2", "Zone 2", "Zone 2",
|
||||
"Zone 3", "Zone 3", "Zone 3",
|
||||
"Zone 4", "Zone 4", "Zone 4", "Zone 4", "Zone 4", "Zone 4", "Zone 4", "Zone 4",
|
||||
"Zone 5", "Zone 5", "Zone 5", "Zone 5", "Zone 5", "Zone 5", "Zone 5", "Zone 5"
|
||||
};
|
||||
String[] prices = {
|
||||
"3.9",
|
||||
"7.58", "7.58", "7.58", "7.58", "7.58",
|
||||
"7.72", "7.72", "7.72",
|
||||
"8.41", "8.41", "8.41", "8.41", "8.41", "8.41", "8.41", "8.41",
|
||||
"9.45", "9.45", "9.45", "9.45", "9.45", "9.45", "9.45", "9.45"
|
||||
};
|
||||
assertEquals(codes.length, ups.getWeightBands().size());
|
||||
for (int i = 0; i < codes.length; i++)
|
||||
{
|
||||
PayWeightBand band = ups.getWeightBands().get(i);
|
||||
assertEquals(zones[i], band.getZoneName(), codes[i]);
|
||||
assertEquals(codes[i], band.getCountryCode());
|
||||
assertEquals(codes[i], band.getCountryName());
|
||||
assertTrue(decimal(band, prices[i]), codes[i] + " " + prices[i]);
|
||||
}
|
||||
assertTrue(ups.getSurcharges().stream().anyMatch(fee -> "报价说明".equals(fee.getFeeName())
|
||||
&& fee.getChargeText().contains("不含管理费") && fee.getChargeText().contains("不含燃油")));
|
||||
PaySurcharge handling = ups.getSurcharges().stream()
|
||||
.filter(fee -> fee.getFeeName().startsWith("额外处理费") && (fee.getSectionName() == null || !fee.getSectionName().contains("高峰")))
|
||||
.findFirst().orElseThrow();
|
||||
assertTrue(handling.getChargeText().contains("23.65") && handling.getChargeText().contains("26.7"));
|
||||
assertTrue(handling.getFeeDesc().contains("是否加收燃油:是"));
|
||||
PaySurcharge peak = ups.getSurcharges().stream()
|
||||
.filter(fee -> "高峰附加费".equals(fee.getSectionName()) && fee.getFeeName().startsWith("额外处理费"))
|
||||
.findFirst().orElseThrow();
|
||||
assertTrue(peak.getChargeText().contains("7.35"));
|
||||
assertEquals(java.time.LocalDate.of(2026, 9, 28),
|
||||
peak.getEffectiveStart().toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDate());
|
||||
assertEquals(java.time.LocalDate.of(2027, 1, 17),
|
||||
peak.getEffectiveEnd().toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDate());
|
||||
|
||||
PayProduct colissimo = product(preview, "Colissimo");
|
||||
PaySurcharge fuel = colissimo.getSurcharges().stream().filter(fee -> fee.getFeeName().contains("燃油")).findFirst().orElseThrow();
|
||||
assertTrue(fuel.getChargeText().contains("不含燃油"));
|
||||
assertTrue(fuel.getChargeText().contains("不含去碳"));
|
||||
assertTrue(colissimo.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().startsWith("去碳附加费") && decimalFee(fee, "0.05")));
|
||||
assertTrue(colissimo.getWeightBands().stream().anyMatch(band -> decimal(band, "3.03")));
|
||||
assertTrue(colissimo.getWeightBands().stream().noneMatch(band -> band.getChargeName() != null && band.getChargeName().contains("燃油")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void imilePostcodesResolveZoneAndGermanCoverage() throws Exception
|
||||
{
|
||||
PayPreview preview = parse("32、");
|
||||
PayProduct italy = product(preview, "IT-iMile");
|
||||
assertTrue(italy.getRelatedSheet().contains("iMile邮编分区"));
|
||||
assertTrue(italy.getExtraLines().size() > 4000);
|
||||
assertTrue(italy.getExtraLines().stream().anyMatch(line -> "邮编".equals(line.getLineType()) && line.getLineText().contains("00010") && line.getLineText().contains("Province") && line.getLineText().contains("ROMA")));
|
||||
assertTrue(italy.getExtraLines().stream().anyMatch(line -> line.getLineText().endsWith(" | City")));
|
||||
assertTrue(italy.getExtraLines().stream().anyMatch(line -> line.getLineText().endsWith(" | Rural")));
|
||||
assertTrue(italy.getWeightBands().stream().anyMatch(band -> "City".equals(band.getZoneName()) && decimal(band, "2.85")));
|
||||
assertTrue(italy.getWeightBands().stream().anyMatch(band -> "Rural".equals(band.getZoneName()) && decimal(band, "3.68")));
|
||||
|
||||
PayProduct germany = product(preview, "DE-iMile");
|
||||
assertTrue(germany.getRelatedSheet().contains("iMile邮编分区"));
|
||||
assertTrue(germany.getExtraLines().size() > 7000);
|
||||
assertTrue(germany.getExtraLines().stream().anyMatch(line -> line.getLineText().contains("01001") && line.getLineText().contains("Active")));
|
||||
assertTrue(germany.getExtraLines().stream().noneMatch(line -> line.getLineText().contains("Lazio") || line.getLineText().contains("City")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gweFileOverridesOnlyPanEuroSupplier() throws Exception
|
||||
{
|
||||
PayPreview preview = parse("8.1");
|
||||
PayProduct dhl = product(preview, "DHL泛欧");
|
||||
assertEquals("GWE", dhl.getSupplierName());
|
||||
assertTrue(dhl.getWeightBands().stream().anyMatch(band -> decimal(band, "9.79")),
|
||||
dhl.getParseStatus() + " sur=" + dhl.getSurchargeCount() + " extra=" + dhl.getExtraCount() + " " + dhl.getRemark() + " "
|
||||
+ dhl.getWeightBands().stream().limit(8)
|
||||
.map(band -> band.getCountryName() + "/" + band.getWeightLabel() + "/" + band.getChargeName() + "/" + band.getPrice()).toList());
|
||||
PayProduct ctt = product(preview, "CTT");
|
||||
assertEquals("威狮", ctt.getSupplierName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void wholeWorkbookPreviewSerializes() throws Exception
|
||||
{
|
||||
PayPreview preview = parse("9、");
|
||||
String json = tools.jackson.databind.json.JsonMapper.builder().build().writeValueAsString(preview);
|
||||
assertTrue(json.contains("\"productName\":\"Evri 48H\""));
|
||||
assertTrue(json.contains("2026-10-01") || json.contains("effectiveDate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void yideCatalogIncludesPostcodeProducts() throws Exception
|
||||
{
|
||||
PayPreview preview = parse("26、");
|
||||
assertEquals(10, preview.getProducts().size());
|
||||
assertTrue(preview.getProducts().stream().anyMatch(item -> item.getExtraCount() != null && item.getExtraCount() > 0));
|
||||
PayProduct evri = preview.getProducts().stream().filter(item -> item.getSheetName().contains("英国evri")).findFirst().orElseThrow();
|
||||
assertFalse(evri.getWeightBands().isEmpty());
|
||||
assertFalse(evri.getSurcharges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dictMatchReadsNamedSheetAndSkipsOthers() throws Exception
|
||||
{
|
||||
Path file = priceCard("Evri 48H.xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "Evri 48H",
|
||||
List.of("Evri 48H", "DHL Paket"));
|
||||
assertEquals(1, preview.getProducts().size());
|
||||
PayProduct evri = preview.getProducts().get(0);
|
||||
assertEquals("Evri 48H", evri.getDictValue());
|
||||
assertEquals("Evri 48H", evri.getSheetName());
|
||||
assertFalse(evri.getWeightBands().isEmpty());
|
||||
assertTrue(evri.getWeightBands().stream().allMatch(band -> "派送费".equals(band.getChargeName())));
|
||||
assertTrue(evri.getWeightBands().stream().noneMatch(band -> bandText(band).contains("偏远") || bandText(band).contains("Highlands")));
|
||||
assertTrue(evri.getWeightBands().stream().anyMatch(band -> decimal(band, "1.51") && band.getZoneName().contains("Mainland")));
|
||||
PaySurcharge highlands = evri.getSurcharges().stream()
|
||||
.filter(fee -> "偏远附加费".equals(fee.getFeeName()) && fee.getZoneName().contains("Highlands") && decimalFee(fee, "5.95"))
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("ZONE", highlands.getCalcRule());
|
||||
assertTrue(highlands.getWeightMax() != null && highlands.getWeightMax().compareTo(java.math.BigDecimal.ONE) == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void panEuroKeepsTwoChargesPerCountryAndEverySurcharge() throws Exception
|
||||
{
|
||||
Path file = Path.of("D:/payment/DHL泛欧.xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayProduct dhl = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "DHL泛欧",
|
||||
List.of("DHL泛欧")).getProducts().get(0);
|
||||
assertPanEuroPrices(dhl);
|
||||
assertEquals(15, dhl.getSurcharges().size(), dhl.getSurcharges().stream()
|
||||
.map(fee -> fee.getFeeName() + "/" + fee.getChargeText()).toList().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void panEuroSheetInCatalogDoesNotCopyPricesAndKeepsHighRisk() throws Exception
|
||||
{
|
||||
Path dir = Path.of("D:/应付供应商");
|
||||
Assumptions.assumeTrue(Files.isDirectory(dir));
|
||||
Path file;
|
||||
try (Stream<Path> files = Files.list(dir))
|
||||
{
|
||||
file = files.filter(path -> path.getFileName().toString().startsWith("8、") && path.toString().endsWith(".xlsx"))
|
||||
.findFirst().orElseThrow();
|
||||
}
|
||||
PayProduct dhl = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "DHL泛欧",
|
||||
List.of("DHL泛欧")).getProducts().get(0);
|
||||
assertPanEuroPrices(dhl);
|
||||
assertTrue(dhl.getRelatedSheet() != null && dhl.getRelatedSheet().contains("高风险"));
|
||||
PaySurcharge afghan = dhl.getSurcharges().stream()
|
||||
.filter(fee -> "高风险地区附加费".equals(fee.getFeeName()) && "AF".equals(fee.getCalcParam())).findFirst().orElseThrow();
|
||||
assertEquals("Afghanistan", afghan.getZoneName());
|
||||
assertTrue(decimalFee(afghan, "10.4"));
|
||||
assertTrue(dhl.getSurcharges().stream().filter(fee -> "高风险地区附加费".equals(fee.getFeeName()) && "AF".equals(fee.getCalcParam())).count() == 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nlDhlVolumeStaysOnTheWeightBand() throws Exception
|
||||
{
|
||||
Path file = Path.of("D:/payment/NL-DHL.xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "NL-DHL",
|
||||
List.of("NL-DHL"));
|
||||
PayProduct dhl = preview.getProducts().get(0);
|
||||
assertEquals(10, dhl.getWeightBands().size(), dhl.getWeightBands().stream()
|
||||
.map(band -> band.getWeightLabel() + "/" + band.getChargeName() + "/" + band.getZoneName() + "/" + band.getPrice()
|
||||
+ "/" + band.getVolumeText())
|
||||
.toList().toString());
|
||||
assertTrue(dhl.getWeightBands().stream().noneMatch(band -> textOf(band.getChargeName()).contains("长")
|
||||
|| textOf(band.getZoneName()).contains("长")));
|
||||
assertTrue(dhl.getWeightBands().stream().allMatch(band -> band.getVolumeText() != null && band.getVolumeText().endsWith("cm³")));
|
||||
assertTrue(dhl.getWeightBands().stream().anyMatch(band -> "0<W≤5".equals(band.getWeightLabel())
|
||||
&& decimal(band, "3.49") && "10000cm³".equals(band.getVolumeText())
|
||||
&& "处理费".equals(band.getChargeName()) && "EUR/ITEM".equals(band.getPriceUnit())));
|
||||
assertTrue(dhl.getWeightBands().stream().anyMatch(band -> "20<W≤31.5".equals(band.getWeightLabel())
|
||||
&& decimal(band, "11.38") && "432000cm³".equals(band.getVolumeText())));
|
||||
assertTrue(dhl.getWeightBands().stream().anyMatch(band -> "0<W≤5".equals(band.getWeightLabel())
|
||||
&& decimal(band, "4.93") && "10000cm³".equals(band.getVolumeText())
|
||||
&& band.getCountryName().contains("比利时") && "BE".equals(band.getCountryCode())));
|
||||
assertTrue(dhl.getWeightBands().stream().anyMatch(band -> "20<W≤31.5".equals(band.getWeightLabel())
|
||||
&& decimal(band, "19.88") && "432000cm³".equals(band.getVolumeText())));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().startsWith("超尺寸附加费")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void royalMailReadsRemoteAreaTableAndSkipsRemarks() throws Exception
|
||||
{
|
||||
Path file = priceCard("英国RM.xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "英国RM", List.of("英国RM"));
|
||||
assertEquals(1, preview.getProducts().size());
|
||||
PayProduct rm = preview.getProducts().get(0);
|
||||
assertEquals(4, rm.getWeightBands().size());
|
||||
assertTrue(rm.getWeightBands().stream().anyMatch(band -> "0-5".equals(band.getWeightLabel()) && decimal(band, "1.62")
|
||||
&& band.getWeightMin() != null && band.getWeightMin().compareTo(java.math.BigDecimal.ZERO) == 0
|
||||
&& band.getWeightMax() != null && band.getWeightMax().compareTo(new java.math.BigDecimal("5")) == 0
|
||||
&& "每票".equals(band.getChargeName()) && "GBP/票".equals(band.getPriceUnit())));
|
||||
assertTrue(rm.getWeightBands().stream().anyMatch(band -> "15.001-20".equals(band.getWeightLabel()) && decimal(band, "5.18")
|
||||
&& band.getWeightMin() != null && band.getWeightMin().compareTo(new java.math.BigDecimal("15.001")) == 0
|
||||
&& band.getWeightMax() != null && band.getWeightMax().compareTo(new java.math.BigDecimal("20")) == 0));
|
||||
List<PaySurcharge> remote = rm.getSurcharges().stream().filter(fee -> "偏远地区附加费".equals(fee.getFeeName())).toList();
|
||||
assertEquals(3, remote.size(), remote.stream().map(fee -> fee.getChargeText() + " / " + fee.getFeeDesc()).toList().toString());
|
||||
assertTrue(remote.stream().anyMatch(fee -> fee.getChargeText().contains("1.80") && fee.getFeeDesc().contains("Area A")
|
||||
&& fee.getFeeDesc().contains("Highlands") && fee.getFeeDesc().contains("IV")));
|
||||
assertTrue(remote.stream().anyMatch(fee -> fee.getChargeText().contains("1.80") && fee.getFeeDesc().contains("Isle of Man")
|
||||
&& fee.getFeeDesc().contains("ZE")));
|
||||
assertTrue(remote.stream().anyMatch(fee -> fee.getChargeText().contains("1.00") && fee.getFeeDesc().contains("Area B")
|
||||
&& fee.getFeeDesc().contains("BT")));
|
||||
assertTrue(remote.stream().allMatch(fee -> "POSTCODE".equals(fee.getCalcRule())));
|
||||
assertTrue(remote.stream().anyMatch(fee -> fee.getCalcParam() != null && fee.getCalcParam().contains("IV")));
|
||||
assertTrue(remote.stream().anyMatch(fee -> fee.getCalcParam() != null && fee.getCalcParam().contains("BT")));
|
||||
assertTrue(rm.getSurcharges().stream().anyMatch(fee -> "签名费".equals(fee.getFeeName()) && fee.getChargeText().contains("0.7")
|
||||
&& "MANUAL".equals(fee.getCalcRule())));
|
||||
assertTrue(rm.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("超大附加费") && fee.getFeeDesc().contains("61")
|
||||
&& "SIZE".equals(fee.getCalcRule()) && fee.getCalcParam() != null && fee.getCalcParam().contains("box:61")));
|
||||
assertTrue(rm.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("超重")
|
||||
&& "WEIGHT".equals(fee.getCalcRule()) && "20".equals(fee.getCalcParam())));
|
||||
assertTrue(rm.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("重复单号") && fee.getChargeText().contains("23")));
|
||||
assertTrue(rm.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("未预报") && fee.getChargeText().contains("1.50")));
|
||||
assertTrue(rm.getSurcharges().stream().noneMatch(fee -> fee.getFeeDesc() != null && (fee.getFeeDesc().contains("计费重量")
|
||||
|| fee.getFeeDesc().contains("赔偿条款") || fee.getFeeDesc().contains("royalmail.com"))));
|
||||
assertTrue(rm.getServiceNote() == null || rm.getServiceNote().isBlank());
|
||||
assertTrue(rm.getExtraLines().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void royalMail48hRemoteRowsMatchPostcodePrefixes() throws Exception
|
||||
{
|
||||
Path file = priceCard("Royal Mail 48H.xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayProduct rm = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "Royal Mail 48H",
|
||||
List.of("Royal Mail 48H")).getProducts().get(0);
|
||||
List<PaySurcharge> remote = rm.getSurcharges().stream().filter(fee -> fee.getFeeName() != null && fee.getFeeName().contains("偏远")).toList();
|
||||
assertFalse(remote.isEmpty(), rm.getSurcharges().stream().map(fee -> fee.getFeeName() + "/" + fee.getCalcRule() + "/" + fee.getCalcParam()).toList().toString());
|
||||
assertTrue(remote.stream().allMatch(fee -> "POSTCODE".equals(fee.getCalcRule())),
|
||||
remote.stream().map(fee -> fee.getFeeName() + "/" + fee.getCalcParam() + "/" + fee.getFeeDesc()).toList().toString());
|
||||
assertTrue(remote.stream().anyMatch(fee -> fee.getCalcParam() != null && fee.getCalcParam().contains("IV") && !fee.getCalcParam().contains("邮编")));
|
||||
assertTrue(rm.getWeightBands().stream().noneMatch(band -> bandText(band).contains("偏远")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void evriOtherFeesKeepOneRowPerRegion() throws Exception
|
||||
{
|
||||
Path file = Path.of("D:/payment/英国evri.xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "英国evri", List.of("英国evri"));
|
||||
PayProduct evri = preview.getProducts().get(0);
|
||||
List<PaySurcharge> oversize = evri.getSurcharges().stream()
|
||||
.filter(fee -> "超尺寸费".equals(fee.getFeeName())).toList();
|
||||
assertEquals(6, oversize.size(), oversize.stream().map(fee -> fee.getZoneName() + "=" + fee.getChargeText()).toList().toString());
|
||||
assertTrue(oversize.stream().anyMatch(fee -> fee.getZoneName().contains("英国大陆") && fee.getChargeText().contains("6.99")));
|
||||
assertTrue(oversize.stream().anyMatch(fee -> fee.getZoneName().contains("高地") && fee.getChargeText().contains("9.67")));
|
||||
assertTrue(oversize.stream().anyMatch(fee -> fee.getZoneName().contains("马恩岛") && fee.getChargeText().contains("9.67")));
|
||||
assertTrue(evri.getSurcharges().stream().anyMatch(fee -> "周末服务费".equals(fee.getFeeName())
|
||||
&& fee.getZoneName().contains("北爱尔兰") && fee.getChargeText().contains("3.02")));
|
||||
assertTrue(evri.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().startsWith("旺季附加费")
|
||||
&& fee.getZoneName().contains("英国大陆") && fee.getChargeText().contains("0.15")));
|
||||
assertTrue(evri.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().startsWith("旺季附加费")
|
||||
&& fee.getZoneName().contains("马恩岛") && fee.getChargeText().contains("5.5")));
|
||||
assertTrue(evri.getSurcharges().stream().noneMatch(fee -> fee.getFeeName() != null
|
||||
&& (fee.getFeeName().contains("服务说明") || fee.getFeeName().contains("索赔"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void spainMainlandKeepsAreasAndTwoSurcharges() throws Exception
|
||||
{
|
||||
Path file = Path.of("D:/payment/西邮-西班牙(主岛).xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "西邮-西班牙(主岛)",
|
||||
List.of("西邮-西班牙(主岛)"));
|
||||
PayProduct spain = preview.getProducts().get(0);
|
||||
assertEquals(160, spain.getWeightBands().size());
|
||||
assertTrue(spain.getWeightBands().stream().anyMatch(band -> band.getWeightLabel().contains("0.01-0.25")
|
||||
&& "MAD-BCN Dense".equals(band.getZoneName()) && decimal(band, "2.27") && "每件".equals(band.getChargeName())
|
||||
&& "EUR/件".equals(band.getPriceUnit())));
|
||||
assertTrue(spain.getWeightBands().stream().anyMatch(band -> band.getWeightLabel().contains("0.01-0.25")
|
||||
&& "Rural Resto".equals(band.getZoneName()) && decimal(band, "3.12")));
|
||||
assertTrue(spain.getWeightBands().stream().anyMatch(band -> band.getWeightLabel().contains("29-30")
|
||||
&& "Denso Resto".equals(band.getZoneName()) && decimal(band, "10.08")));
|
||||
assertTrue(spain.getSurcharges().stream().anyMatch(fee -> "重新贴尾程标签附加费".equals(fee.getFeeName())
|
||||
&& fee.getChargeText().contains("0.35") && fee.getFeeDesc().contains("重新贴尾程标签")));
|
||||
List<PaySurcharge> oversize = spain.getSurcharges().stream().filter(fee -> "特大包裹收费".equals(fee.getFeeName())).toList();
|
||||
assertEquals(2, oversize.size(), oversize.stream().map(PaySurcharge::getChargeText).toList().toString());
|
||||
assertTrue(oversize.stream().anyMatch(fee -> fee.getChargeText().contains("35%") && fee.getFeeDesc().contains("120")
|
||||
&& "SIZE".equals(fee.getCalcRule()) && fee.getCalcParam() != null && fee.getCalcParam().contains("join:or")));
|
||||
assertTrue(oversize.stream().anyMatch(fee -> fee.getChargeText().contains("70%")
|
||||
&& "SIZE".equals(fee.getCalcRule()) && fee.getCalcParam() != null && fee.getCalcParam().contains("join:and")));
|
||||
assertTrue(spain.getSurcharges().stream().anyMatch(fee -> "重新贴尾程标签附加费".equals(fee.getFeeName())
|
||||
&& "MANUAL".equals(fee.getCalcRule())));
|
||||
assertTrue(spain.getSurcharges().stream().noneMatch(fee -> fee.getFeeName() != null
|
||||
&& (fee.getFeeName().contains("禁运") || fee.getFeeName().contains("条款"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void spainGlsSplitsCountryCodesAndServiceFees() throws Exception
|
||||
{
|
||||
Path file = Path.of("D:/payment/西班牙GLS.xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "西班牙GLS",
|
||||
List.of("西班牙GLS"));
|
||||
PayProduct gls = preview.getProducts().get(0);
|
||||
assertEquals("OK", gls.getParseStatus());
|
||||
assertEquals(476, gls.getWeightBands().size());
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "DE".equals(band.getCountryCode()) && "德国".equals(band.getCountryName())
|
||||
&& "A".equals(band.getZoneName()) && "1KG".equals(band.getWeightLabel()) && decimal(band, "5.12")
|
||||
&& band.getWeightMin().compareTo(java.math.BigDecimal.ZERO) == 0
|
||||
&& band.getWeightMax().compareTo(java.math.BigDecimal.ONE) == 0
|
||||
&& "每件".equals(band.getChargeName()) && "EUR/件".equals(band.getPriceUnit())));
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "NL".equals(band.getCountryCode()) && "荷兰".equals(band.getCountryName())
|
||||
&& "A".equals(band.getZoneName()) && decimal(band, "5.12")));
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "PL".equals(band.getCountryCode()) && "波兰".equals(band.getCountryName())));
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "FR".equals(band.getCountryCode()) && "法国".equals(band.getCountryName())
|
||||
&& "B".equals(band.getZoneName()) && "1KG".equals(band.getWeightLabel()) && decimal(band, "6.38")));
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "HU".equals(band.getCountryCode()) && "匈牙利".equals(band.getCountryName())
|
||||
&& "D".equals(band.getZoneName()) && "3KG".equals(band.getWeightLabel()) && decimal(band, "8.15")));
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "GB".equals(band.getCountryCode()) && "英国".equals(band.getCountryName())
|
||||
&& "E".equals(band.getZoneName()) && "1KG".equals(band.getWeightLabel()) && decimal(band, "7.11")));
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "FR2".equals(band.getCountryCode()) && "法国-科西嘉岛".equals(band.getCountryName())
|
||||
&& "G".equals(band.getZoneName()) && "1KG".equals(band.getWeightLabel()) && decimal(band, "16.01")));
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "CY".equals(band.getCountryCode()) && "塞浦路斯".equals(band.getCountryName())
|
||||
&& "H".equals(band.getZoneName()) && decimal(band, "13.01")));
|
||||
assertTrue(gls.getWeightBands().stream().anyMatch(band -> "IT".equals(band.getCountryCode()) && "Ad.kg.".equals(band.getWeightLabel())
|
||||
&& decimal(band, "1.07") && "每公斤".equals(band.getChargeName()) && "EUR/公斤".equals(band.getPriceUnit())
|
||||
&& band.getWeightMin() == null));
|
||||
assertEquals(4, gls.getSurcharges().size());
|
||||
assertTrue(gls.getSurcharges().stream().anyMatch(fee -> "取还服务".equals(fee.getFeeName()) && fee.getChargeText().contains("7")
|
||||
&& fee.getFeeDesc().contains("送货上门") && decimalFee(fee, "7")));
|
||||
assertTrue(gls.getSurcharges().stream().anyMatch(fee -> "取货发货服务".equals(fee.getFeeName()) && decimalFee(fee, "7")));
|
||||
assertTrue(gls.getSurcharges().stream().anyMatch(fee -> "现金服务".equals(fee.getFeeName()) && fee.getFeeDesc().contains("交货付款")
|
||||
&& decimalFee(fee, "6")));
|
||||
assertTrue(gls.getSurcharges().stream().anyMatch(fee -> "灵活配送服务".equals(fee.getFeeName()) && "free".equalsIgnoreCase(fee.getChargeText())
|
||||
&& decimalFee(fee, "0") && fee.getFeeDesc().contains("灵活性")));
|
||||
assertTrue(gls.getSurcharges().stream().noneMatch(fee -> fee.getFeeName() != null && (fee.getFeeName().contains("格陵兰")
|
||||
|| fee.getFeeName().contains("转账") || fee.getFeeName().contains("备注") || fee.getFeeName().contains("能源"))));
|
||||
assertTrue(gls.getServiceNote() == null || gls.getServiceNote().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ctt48hFileKeepsSurchargeTablesOutOfWeightBands() throws Exception
|
||||
{
|
||||
Path file = priceCard("CTT 48H.xlsx");
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "CTT 48H",
|
||||
List.of("CTT 48H"));
|
||||
PayProduct ctt = preview.getProducts().get(0);
|
||||
assertTrue(ctt.getWeightBands().stream().allMatch(band -> "派送费".equals(band.getChargeName())),
|
||||
ctt.getWeightBands().stream().map(band -> band.getChargeName() + "/" + band.getZoneName() + "/" + band.getPrice()).toList().toString());
|
||||
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> decimal(band, "2.05") && band.getZoneName().contains("马德里")));
|
||||
assertTrue(ctt.getWeightBands().stream().noneMatch(band -> bandText(band).contains("偏远") || bandText(band).contains("额外")
|
||||
|| bandText(band).toLowerCase().contains("line-haul")));
|
||||
PaySurcharge balearic = ctt.getSurcharges().stream()
|
||||
.filter(fee -> "偏远附加费".equals(fee.getFeeName()) && fee.getZoneName().contains("Balearic") && decimalFee(fee, "7.1"))
|
||||
.findFirst().orElseThrow();
|
||||
assertTrue(balearic.getFeeDesc().contains("首重"));
|
||||
assertEquals("POSTCODE", balearic.getCalcRule());
|
||||
assertTrue(balearic.getCalcParam() != null && balearic.getCalcParam().contains("07"));
|
||||
assertTrue(balearic.getWeightMax() != null && balearic.getWeightMax().compareTo(new java.math.BigDecimal("1")) == 0);
|
||||
PaySurcharge canary = ctt.getSurcharges().stream()
|
||||
.filter(fee -> "偏远附加费".equals(fee.getFeeName()) && fee.getZoneName().contains("Canary") && decimalFee(fee, "4.5")
|
||||
&& fee.getChargeText().contains("KG"))
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("EXTRA_KG", canary.getCalcRule());
|
||||
assertEquals("1", canary.getCalcParam());
|
||||
assertTrue(canary.getPostcodeParam() != null && canary.getPostcodeParam().contains("35") && canary.getPostcodeParam().contains("38"));
|
||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("额外线路运输费") && "MANUAL".equals(fee.getCalcRule())));
|
||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("额外线路运输费") && decimalFee(fee, "0.09")));
|
||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("额外线路运输费")
|
||||
&& fee.getFeeDesc().contains("800公里")));
|
||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "超重附加费".equals(fee.getFeeName()) && decimalFee(fee, "5.98")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void spainCttMainlandReadsBillableConcepts() throws Exception
|
||||
{
|
||||
Path file = priceCard("西班牙CTT带电专线(主要地区).xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(),
|
||||
"西班牙CTT带电专线(主要地区)", List.of("西班牙CTT带电专线(主要地区)"));
|
||||
PayProduct ctt = preview.getProducts().get(0);
|
||||
assertEquals("OK", ctt.getParseStatus());
|
||||
assertEquals("单票价", ctt.getChargeType());
|
||||
assertTrue(ctt.getExtraPerKg() == null || ctt.getExtraPerKg().isBlank());
|
||||
assertEquals(150, ctt.getWeightBands().size());
|
||||
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "1".equals(band.getWeightLabel())
|
||||
&& "Mad High Density".equals(band.getZoneName()) && decimal(band, "2.11")));
|
||||
assertTrue(ctt.getWeightBands().stream().anyMatch(band -> "30".equals(band.getWeightLabel())
|
||||
&& "Portugal".equals(band.getZoneName()) && decimal(band, "11.96")));
|
||||
assertEquals(15, ctt.getSurcharges().size());
|
||||
assertEquals(5, ctt.getSurcharges().stream().filter(fee -> "Extra Line-Haul (*)".equals(fee.getFeeName())).count());
|
||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "Extra Line-Haul (*)".equals(fee.getFeeName())
|
||||
&& "Mad High Density".equals(fee.getZoneName()) && decimalFee(fee, "0.12") && "EUR".equals(fee.getCurrency())
|
||||
&& "MANUAL".equals(fee.getCalcRule())));
|
||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "Extra Line-Haul Additional Kg (**)".equals(fee.getFeeName())
|
||||
&& "Portugal".equals(fee.getZoneName()) && decimalFee(fee, "0.08")));
|
||||
List<PaySurcharge> pudo = ctt.getSurcharges().stream().filter(fee -> "Pudo".equals(fee.getFeeName())).toList();
|
||||
assertEquals(1, pudo.size());
|
||||
assertTrue(pudo.get(0).getChargeText().contains("0.10") && pudo.get(0).getChargeText().contains("欧元"));
|
||||
assertTrue(decimalFee(pudo.get(0), "0.10"));
|
||||
assertEquals("", pudo.get(0).getZoneName() == null ? "" : pudo.get(0).getZoneName());
|
||||
assertEquals(1, ctt.getSurcharges().stream().filter(fee -> fee.getFeeName().contains("COD")).count());
|
||||
assertEquals(1, ctt.getSurcharges().stream().filter(fee -> "Reverse Logistics".equals(fee.getFeeName())).count());
|
||||
assertEquals(1, ctt.getSurcharges().stream().filter(fee -> "Fuel rate".equals(fee.getFeeName())).count());
|
||||
assertEquals(1, ctt.getSurcharges().stream().filter(fee -> "Volumetric weight".equals(fee.getFeeName())).count());
|
||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("COD")
|
||||
&& fee.getChargeText().contains("1%") && fee.getAmount() == null));
|
||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "Volumetric weight".equals(fee.getFeeName())
|
||||
&& fee.getChargeText().contains("90cm") && fee.getAmount() == null && "MANUAL".equals(fee.getCalcRule())));
|
||||
assertTrue(ctt.getSurcharges().stream().noneMatch(fee -> fee.getFeeName() != null && (fee.getFeeName().contains("计费重量")
|
||||
|| fee.getFeeName().contains("4.95") || fee.getFeeName().contains("丢货") || fee.getFeeName().contains("退件"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
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带电专线(岛屿+其他地区)"));
|
||||
PayProduct ctt = preview.getProducts().get(0);
|
||||
assertEquals("OK", ctt.getParseStatus());
|
||||
assertEquals("EUR", ctt.getCurrency());
|
||||
assertEquals("续重计费", ctt.getChargeType());
|
||||
assertTrue(covers(ctt.getExtraPerKg(), "1.86"));
|
||||
assertTrue(covers(ctt.getExtraPerKg(), "5.79"));
|
||||
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().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.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.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 -> "Cubic Capacity".equals(fee.getFeeName())
|
||||
&& "2. INSULAR RATES / EXPRESS".equals(fee.getSectionName())
|
||||
&& fee.getFeeDesc().contains("Portuguese Island / Island")));
|
||||
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"));
|
||||
PayProduct ctt = preview.getProducts().get(0);
|
||||
assertEquals("OK", ctt.getParseStatus());
|
||||
assertEquals("西班牙", ctt.getCountryScope());
|
||||
assertEquals("续重计费", ctt.getChargeType());
|
||||
assertTrue(covers(ctt.getExtraPerKg(), "0.82"));
|
||||
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())
|
||||
&& band.getWeightMin().compareTo(java.math.BigDecimal.ZERO) == 0
|
||||
&& band.getWeightMax().compareTo(java.math.BigDecimal.ONE) == 0));
|
||||
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().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")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void posteItalianeKeepsTwoDeliveryServicesAndSizeRules() throws Exception
|
||||
{
|
||||
Path file = Path.of("D:/payment/PosteItaliane.xlsx");
|
||||
Assumptions.assumeTrue(Files.exists(file));
|
||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), null, List.of("PosteItaliane"));
|
||||
PayProduct poste = preview.getProducts().get(0);
|
||||
assertEquals("OK", poste.getParseStatus());
|
||||
assertEquals(18, poste.getWeightBands().size(), poste.getWeightBands().stream()
|
||||
.map(band -> band.getChargeName() + "/" + band.getZoneName() + "/" + band.getWeightLabel() + "/" + band.getPrice())
|
||||
.toList().toString());
|
||||
assertEquals(9, poste.getWeightBands().stream().filter(band -> "派送费 Crono Express".equals(band.getChargeName())).count());
|
||||
assertEquals(9, poste.getWeightBands().stream().filter(band -> "派送费 Crono".equals(band.getChargeName())).count());
|
||||
assertTrue(poste.getWeightBands().stream().anyMatch(band -> "派送费 Crono Express".equals(band.getChargeName())
|
||||
&& "City".equals(band.getZoneName()) && band.getWeightLabel().contains("0") && decimal(band, "3")
|
||||
&& "EUR/ITEM".equals(band.getPriceUnit()) && "意大利(IT)".equals(band.getCountryName()) && "IT".equals(band.getCountryCode())));
|
||||
assertTrue(poste.getWeightBands().stream().anyMatch(band -> "派送费 Crono Express".equals(band.getChargeName())
|
||||
&& "Province".equals(band.getZoneName()) && band.getWeightLabel().contains("5") && decimal(band, "6.63")));
|
||||
assertTrue(poste.getWeightBands().stream().anyMatch(band -> "派送费 Crono".equals(band.getChargeName())
|
||||
&& "Rural".equals(band.getZoneName()) && band.getWeightLabel().contains("2") && decimal(band, "4.38")
|
||||
&& "意大利(IT)".equals(band.getCountryName())));
|
||||
assertTrue(poste.getWeightBands().stream().anyMatch(band -> "派送费 Crono".equals(band.getChargeName())
|
||||
&& "Province".equals(band.getZoneName()) && band.getWeightLabel().contains("5") && decimal(band, "5.83")));
|
||||
assertTrue(poste.getExtraLines().stream().anyMatch(line -> "配置".equals(line.getLineType())
|
||||
&& line.getLineText().contains("尺寸限制") && line.getLineText().contains("长+宽+高≤130cm")
|
||||
&& line.getLineText().contains("最长边≤60cm")));
|
||||
assertTrue(poste.getExtraLines().stream().anyMatch(line -> line.getLineText().contains("计费说明") && line.getLineText().contains("计费重")));
|
||||
assertTrue(poste.getSurcharges().stream().anyMatch(fee -> "销毁费".equals(fee.getFeeName()) && decimalFee(fee, "0.2")
|
||||
&& "EUR/ITEM".equals(fee.getPriceUnit()) && "产生时收取".equals(fee.getChargeTiming())));
|
||||
assertTrue(poste.getSurcharges().stream().anyMatch(fee -> "退件重派费".equals(fee.getFeeName())
|
||||
&& fee.getChargeText().contains("同正向运费") && "产生时收取".equals(fee.getChargeTiming())));
|
||||
assertTrue(poste.getSurcharges().stream().filter(fee -> "产生时收取".equals(fee.getChargeTiming())).count() >= 6);
|
||||
}
|
||||
|
||||
private PayPreview parse(String prefix) throws Exception
|
||||
{
|
||||
Path dir = Path.of("D:/应付供应商");
|
||||
Path file;
|
||||
try (Stream<Path> files = Files.list(dir))
|
||||
{
|
||||
file = files.filter(path -> path.getFileName().toString().startsWith(prefix) && path.toString().endsWith(".xlsx"))
|
||||
.findFirst().orElseThrow();
|
||||
}
|
||||
return parser.parse(Files.readAllBytes(file), file.getFileName().toString());
|
||||
}
|
||||
|
||||
private void assertPanEuroPrices(PayProduct dhl)
|
||||
{
|
||||
assertEquals(52, dhl.getWeightBands().size(), dhl.getWeightBands().stream()
|
||||
.map(band -> band.getCountryCode() + "/" + band.getChargeName() + "/" + band.getPrice()).toList().toString());
|
||||
assertEquals(26, dhl.getWeightBands().stream().map(PayWeightBand::getCountryCode).distinct().count());
|
||||
assertEquals(26, dhl.getWeightBands().stream().filter(band -> "处理费".equals(band.getChargeName())).count());
|
||||
assertEquals(26, dhl.getWeightBands().stream().filter(band -> "速递运费".equals(band.getChargeName())).count());
|
||||
assertTrue(dhl.getWeightBands().stream().anyMatch(band -> "DK".equals(band.getCountryCode())
|
||||
&& "处理费".equals(band.getChargeName()) && decimal(band, "9.71") && "EUR/ITEM".equals(band.getPriceUnit())));
|
||||
assertTrue(dhl.getWeightBands().stream().anyMatch(band -> "DK".equals(band.getCountryCode())
|
||||
&& "速递运费".equals(band.getChargeName()) && decimal(band, "0.23") && "EUR/KG".equals(band.getPriceUnit())));
|
||||
assertTrue(dhl.getServiceNote() != null && dhl.getServiceNote().contains("报价已含燃油"));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "其他费用".equals(fee.getSectionName())
|
||||
&& "道路通行费".equals(fee.getFeeName()) && decimalFee(fee, "0.19")
|
||||
&& "EUR".equals(fee.getCurrency()) && "EUR/ITEM".equals(fee.getPriceUnit())));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "旺季附加费".equals(fee.getFeeName()) && decimalFee(fee, "0.19")
|
||||
&& fee.getEffectiveStart() != null));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "高峰旺季附加费".equals(fee.getFeeName()) && decimalFee(fee, "0.5")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "超尺寸超重费".equals(fee.getFeeName()) && decimalFee(fee, "14")
|
||||
&& "产生时收取".equals(fee.getChargeTiming())));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "路由码选择附加费".equals(fee.getFeeName()) && decimalFee(fee, "0.49")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "重量信息修正附加费".equals(fee.getFeeName()) && decimalFee(fee, "0.99")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "未预报附加费".equals(fee.getFeeName())
|
||||
&& fee.getChargeText().contains("31.5kg")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "托收附加费".equals(fee.getFeeName()) && decimalFee(fee, "2.2")
|
||||
&& textOf(fee.getFeeDesc()).contains("Supplemental")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "提供虚假信息罚款费".equals(fee.getFeeName()) && decimalFee(fee, "18")
|
||||
&& textOf(fee.getFeeDesc()).contains("故意")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "高风险地区附加费".equals(fee.getFeeName())
|
||||
&& textOf(fee.getChargeText()).contains("高风险")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "退件费".equals(fee.getFeeName()) && decimalFee(fee, "20")
|
||||
&& textOf(fee.getFeeDesc()).contains("空运")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "退件费".equals(fee.getFeeName()) && decimalFee(fee, "10")
|
||||
&& textOf(fee.getFeeDesc()).contains("陆运")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "重派费".equals(fee.getFeeName())
|
||||
&& textOf(fee.getChargeText()).contains("同正向运费")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "退件操作费".equals(fee.getFeeName()) && decimalFee(fee, "0.5")));
|
||||
assertTrue(dhl.getSurcharges().stream().anyMatch(fee -> "重派操作费".equals(fee.getFeeName()) && decimalFee(fee, "1")));
|
||||
}
|
||||
|
||||
private PayProduct product(PayPreview preview, String name)
|
||||
{
|
||||
return preview.getProducts().stream().filter(item -> name.equals(item.getProductName())).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private boolean covers(String extraPerKg, String price)
|
||||
{
|
||||
if (extraPerKg == null || extraPerKg.isBlank())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
java.math.BigDecimal value = new java.math.BigDecimal(price);
|
||||
String[] parts = extraPerKg.split("~");
|
||||
java.math.BigDecimal low = new java.math.BigDecimal(parts[0]);
|
||||
java.math.BigDecimal high = new java.math.BigDecimal(parts[parts.length - 1]);
|
||||
return low.compareTo(value) <= 0 && high.compareTo(value) >= 0;
|
||||
}
|
||||
|
||||
private String textOf(String value)
|
||||
{
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private boolean decimal(PayWeightBand band, String price)
|
||||
{
|
||||
return band.getPrice() != null && band.getPrice().compareTo(new java.math.BigDecimal(price)) == 0;
|
||||
}
|
||||
|
||||
private boolean decimalFee(PaySurcharge fee, String amount)
|
||||
{
|
||||
return fee.getAmount() != null && fee.getAmount().compareTo(new java.math.BigDecimal(amount)) == 0;
|
||||
}
|
||||
|
||||
private static Path priceCard(String name)
|
||||
{
|
||||
Path current = Path.of("D:/A-payment").resolve(name);
|
||||
if (Files.exists(current))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
return Path.of("D:/payment").resolve(name);
|
||||
}
|
||||
|
||||
private String bandText(PayWeightBand band)
|
||||
{
|
||||
return (band.getChargeName() == null ? "" : band.getChargeName()) + " "
|
||||
+ (band.getSectionName() == null ? "" : band.getSectionName()) + " "
|
||||
+ (band.getZoneName() == null ? "" : band.getZoneName());
|
||||
}
|
||||
|
||||
private String zoneTail(PayWeightBand band)
|
||||
{
|
||||
String zone = band.getZoneName() == null ? "" : band.getZoneName();
|
||||
return zone.contains("英国大陆") ? "英国大陆" : zone;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.bms.price.pay;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.price.domain.PayPostcodeLine;
|
||||
|
||||
class PayZoneMatcherTest
|
||||
{
|
||||
private static final List<String> ZONES = List.of(
|
||||
"UK Mainland\n英国大陆",
|
||||
"Highlands& Islands\n高地和岛屿",
|
||||
"Channel Islands\n(海峡群岛,包括Isle of Scilly锡利岛)",
|
||||
"Northern Ireland\n北爱尔兰",
|
||||
"BFPO\n(军事地址)",
|
||||
"Isle of Man\n马恩岛");
|
||||
|
||||
@Test
|
||||
void welshPostcodeFallsBackToMainland()
|
||||
{
|
||||
PayZoneMatcher.Decision decision = PayZoneMatcher.resolve(ZONES, evriLines(), "LL22 8PJ");
|
||||
assertNull(decision.getMessage());
|
||||
assertEquals("UK Mainland\n英国大陆", decision.getZoneName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void northernIrelandAndHighlandsComeFromThePostcodeBook()
|
||||
{
|
||||
assertEquals("Northern Ireland\n北爱尔兰", PayZoneMatcher.resolve(ZONES, evriLines(), "BT1 1AA").getZoneName());
|
||||
assertEquals("Highlands& Islands\n高地和岛屿", PayZoneMatcher.resolve(ZONES, evriLines(), "IV1 1AA").getZoneName());
|
||||
assertEquals("Channel Islands\n(海峡群岛,包括Isle of Scilly锡利岛)",
|
||||
PayZoneMatcher.resolve(ZONES, evriLines(), "TR21 1AA").getZoneName());
|
||||
assertEquals("Isle of Man\n马恩岛", PayZoneMatcher.resolve(ZONES, evriLines(), "IM1 1AA").getZoneName());
|
||||
assertEquals("BFPO\n(军事地址)", PayZoneMatcher.resolve(ZONES, evriLines(), "BF1 0AA").getZoneName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void productNameKeepsTheCarrierToken()
|
||||
{
|
||||
assertEquals(List.of("evri"), PayZoneMatcher.searchTokens("英国evri"));
|
||||
assertEquals(List.of("ubi", "evri"), PayZoneMatcher.searchTokens("UBI英国EVRi普"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingPostcodeIsRejected()
|
||||
{
|
||||
PayZoneMatcher.Decision decision = PayZoneMatcher.resolve(ZONES, evriLines(), " ");
|
||||
assertTrue(decision.getMessage().contains("没有邮编"));
|
||||
}
|
||||
|
||||
private static List<PayPostcodeLine> evriLines()
|
||||
{
|
||||
return List.of(
|
||||
line("Channel Islands", "GY"),
|
||||
line("Isle of Man", "IM"),
|
||||
line("Northern Ireland", "BT"),
|
||||
line("Scilly Isles", "TR21"),
|
||||
line("Scottish H&I (Finance)", "IV"),
|
||||
line("Scottish H&I (Finance)", "HS"),
|
||||
line("Shetlands", "ZE"));
|
||||
}
|
||||
|
||||
private static PayPostcodeLine line(String region, String postcode)
|
||||
{
|
||||
PayPostcodeLine line = new PayPostcodeLine();
|
||||
line.setRegionName(region);
|
||||
line.setPostcode(postcode);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.domain.ArBalanceWriteoff;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
import com.bms.ar.mapper.ArCommissionMapper;
|
||||
import com.bms.ar.profit.ArCommissionRate;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.ar.profit.ArCommissionService;
|
||||
|
||||
class ArCommissionServiceTest
|
||||
{
|
||||
@Test
|
||||
void snapshotsTheRateOnceAtWriteoffAndPreservesMissingRates()
|
||||
{
|
||||
List<String> calls = new ArrayList<>();
|
||||
AtomicReference<Object[]> inserted = new AtomicReference<>();
|
||||
AtomicReference<ArCommissionRate> effective = new AtomicReference<>();
|
||||
ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] { ArCommissionMapper.class }, (proxy, method, args) -> {
|
||||
calls.add(method.getName());
|
||||
return switch (method.getName()) {
|
||||
case "lockSalesUser" -> 7L;
|
||||
case "selectEffective" -> effective.get();
|
||||
case "insertWriteoffRate" -> { inserted.set(args.clone()); yield 1; }
|
||||
default -> throw new AssertionError(method.getName());
|
||||
};
|
||||
});
|
||||
ArCommissionService service = new ArCommissionService(mapper, null, null, null);
|
||||
ArBill bill = new ArBill();
|
||||
bill.setProfitSalesUserId(7L);
|
||||
ArBalanceWriteoff writeoff = new ArBalanceWriteoff();
|
||||
writeoff.setWriteoffId(18L);
|
||||
ArCommissionRate rate = new ArCommissionRate();
|
||||
rate.setRateId(3L);
|
||||
rate.setRate(new BigDecimal("10"));
|
||||
effective.set(rate);
|
||||
service.snapshotWriteoff(bill, writeoff);
|
||||
assertEquals(List.of("lockSalesUser", "selectEffective", "insertWriteoffRate"), calls);
|
||||
assertEquals(3L, inserted.get()[2]);
|
||||
assertEquals(new BigDecimal("10"), inserted.get()[3]);
|
||||
|
||||
calls.clear();
|
||||
effective.set(null);
|
||||
service.snapshotWriteoff(bill, writeoff);
|
||||
assertNull(inserted.get()[2]);
|
||||
assertNull(inserted.get()[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void savesOnlyCalculatedAmountsForReadyRows()
|
||||
{
|
||||
ArCommissionResult ready = new ArCommissionResult();
|
||||
ready.setResultStatus("READY");
|
||||
ready.setGrossProfit(new BigDecimal("-40"));
|
||||
ready.setWeightedRate(new BigDecimal("1000"));
|
||||
ready.setBillAmount(new BigDecimal("100"));
|
||||
ArCommissionResult missing = new ArCommissionResult();
|
||||
missing.setResultStatus("MISSING_PRICE");
|
||||
List<ArCommissionResult> saved = new ArrayList<>();
|
||||
ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] { ArCommissionMapper.class }, (proxy, method, args) -> {
|
||||
if ("selectSettlementInputs".equals(method.getName())) return List.of(ready, missing);
|
||||
if ("insertResults".equals(method.getName())) {
|
||||
@SuppressWarnings("unchecked") List<ArCommissionResult> batch = (List<ArCommissionResult>) args[0];
|
||||
saved.addAll(batch);
|
||||
return batch.size();
|
||||
}
|
||||
throw new AssertionError(method.getName());
|
||||
});
|
||||
new ArCommissionService(mapper, null, null, null).createFinalResults(1L, "finance");
|
||||
assertEquals(new BigDecimal("-4.00"), saved.get(0).getCommissionAmount());
|
||||
assertNull(saved.get(1).getCommissionAmount());
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.profit.ArProfitQuery;
|
||||
import com.bms.ar.profit.ArProfitScope;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
|
||||
class ArProfitMapperSqlTest
|
||||
{
|
||||
@Test
|
||||
void parsesReportAndCommissionMappersAndScopesEachReportQuery() throws Exception
|
||||
{
|
||||
Configuration configuration = new Configuration();
|
||||
for (String resource : new String[] { "mapper/ar/ArProfitMapper.xml", "mapper/ar/ArCommissionMapper.xml" })
|
||||
{
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource))
|
||||
{
|
||||
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
}
|
||||
for (String id : new String[] { "selectBills", "selectSummary", "selectBillDetails", "selectFees" })
|
||||
{
|
||||
String name = "com.bms.ar.mapper.ArProfitMapper." + id;
|
||||
String self = configuration.getMappedStatement(name).getBoundSql(Map.of(
|
||||
"query", new ArProfitQuery(), "scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql();
|
||||
String manager = configuration.getMappedStatement(name).getBoundSql(Map.of(
|
||||
"query", new ArProfitQuery(), "scope", new ArProfitScope(false, false, true, 8L, 10L))).getSql();
|
||||
assertTrue(self.contains("b.profit_sales_user_id = ?"));
|
||||
assertFalse(self.contains("find_in_set"));
|
||||
assertTrue(manager.contains("find_in_set"));
|
||||
assertTrue(manager.contains("b.status in"));
|
||||
}
|
||||
ArProfitQuery orderFilter = new ArProfitQuery();
|
||||
orderFilter.setOrderNo("ORDER-1");
|
||||
String bills = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectBills")
|
||||
.getBoundSql(Map.of("query", orderFilter, "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertTrue(bills.contains("from ar_bill b"));
|
||||
assertTrue(bills.contains("exists (select 1 from ar_bill_detail matched"));
|
||||
assertFalse(bills.contains("and d.order_no like"));
|
||||
String billCommission = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectCommissionSummary")
|
||||
.getBoundSql(Map.of("billId", 9L)).getSql();
|
||||
assertTrue(billCommission.contains("r.ar_bill_id = ?"));
|
||||
assertTrue(billCommission.contains("then null"));
|
||||
var salesSql = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSalesOptions")
|
||||
.getBoundSql(Map.of("keyword", "sales"));
|
||||
assertTrue(salesSql.getSql().contains("from sys_user u"));
|
||||
assertTrue(salesSql.getSql().contains("u.nick_name like"));
|
||||
assertTrue(salesSql.getSql().contains("u.user_name like"));
|
||||
assertFalse(salesSql.getSql().contains("ar_bill"));
|
||||
assertFalse(salesSql.getSql().contains("sys_role"));
|
||||
assertTrue(salesSql.getSql().contains("limit 50"));
|
||||
String settlement = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectSettlementInputs")
|
||||
.getBoundSql(Map.of("billId", 1L)).getSql();
|
||||
assertTrue(settlement.contains("w.status = 'EFFECTIVE'"));
|
||||
assertTrue(settlement.contains("b.status = 'WRITTEN_OFF'"));
|
||||
var insert = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.insertResults")
|
||||
.getBoundSql(Map.of("items", java.util.List.of(new ArCommissionResult(), new ArCommissionResult()), "operator", "test"));
|
||||
assertTrue(insert.getSql().contains("ar_profit_commission_result"));
|
||||
assertTrue(insert.getParameterMappings().size() == 16);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.profit.ArProfitScopeResolver;
|
||||
import com.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
|
||||
class ArProfitScopeResolverTest
|
||||
{
|
||||
private static final String PERMISSION = "report:arProfit:list";
|
||||
|
||||
private LoginUser actor(String... keys)
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(99L);
|
||||
user.setStatus("0");
|
||||
user.setDelFlag("0");
|
||||
user.setRoles(java.util.Arrays.stream(keys).map(key -> {
|
||||
SysRole role = new SysRole();
|
||||
role.setRoleKey(key);
|
||||
role.setStatus("0");
|
||||
role.setPermissions(Set.of(PERMISSION));
|
||||
return role;
|
||||
}).toList());
|
||||
return new LoginUser(99L, 10L, user, Set.of(PERMISSION));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowedRolesHaveOnlyTheirApprovedScope()
|
||||
{
|
||||
ArProfitScopeResolver resolver = new ArProfitScopeResolver();
|
||||
var sales = resolver.resolve(actor("ORA_6041"), PERMISSION);
|
||||
assertTrue(sales.self());
|
||||
assertFalse(sales.all());
|
||||
var manager = resolver.resolve(actor("ORA_6040"), PERMISSION);
|
||||
assertTrue(manager.department());
|
||||
assertFalse(manager.all());
|
||||
assertTrue(resolver.resolve(actor("ORA_6042"), PERMISSION).all());
|
||||
assertTrue(resolver.resolve(actor("ORA_6043"), PERMISSION).all());
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("ORA_6163"), PERMISSION));
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("UNKNOWN"), PERMISSION));
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("ORA_6042"), "other:permission"));
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import com.bms.ar.profit.CommissionCalculator;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.ar.profit.CommissionCalculator.Writeoff;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CommissionCalculatorTest
|
||||
{
|
||||
private static BigDecimal n(String value) { return new BigDecimal(value); }
|
||||
|
||||
@Test
|
||||
void waitsForFullSettlementAndUsesEachWriteoffRate()
|
||||
{
|
||||
var first = new Writeoff(n("50"), n("10"));
|
||||
var partial = CommissionCalculator.calculate(n("100"), n("20"), List.of(first));
|
||||
assertEquals(n("1.00"), partial.estimated());
|
||||
assertNull(partial.formal());
|
||||
var complete = CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(first, new Writeoff(n("50"), n("20"))));
|
||||
assertEquals(n("3.00"), complete.formal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deductsNegativeProfitAndKeepsMissingRatesUnknown()
|
||||
{
|
||||
assertEquals(n("-4.00"), CommissionCalculator.calculate(n("100"), n("-40"),
|
||||
List.of(new Writeoff(n("100"), n("10")))).formal());
|
||||
assertNull(CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(new Writeoff(n("100"), null))).formal());
|
||||
assertThrows(IllegalArgumentException.class, () -> CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(new Writeoff(n("101"), n("10")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocatesRoundingRemainderAcrossOrderResults()
|
||||
{
|
||||
List<ArCommissionResult> orders = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
ArCommissionResult order = new ArCommissionResult();
|
||||
order.setResultStatus("READY");
|
||||
order.setBillAmount(n("100"));
|
||||
order.setGrossProfit(n("0.05"));
|
||||
order.setWeightedRate(n("1000"));
|
||||
orders.add(order);
|
||||
}
|
||||
CommissionCalculator.allocateFinalAmounts(orders);
|
||||
assertEquals(n("0.01"), orders.get(0).getCommissionAmount());
|
||||
assertEquals(n("0.00"), orders.get(1).getCommissionAmount());
|
||||
assertEquals(n("0.01"), orders.get(2).getCommissionAmount());
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.price.domain.vo.PrcProfitReportQuery;
|
||||
import com.bms.price.mapper.PrcProfitReportMapper;
|
||||
import com.bms.price.service.impl.PrcProfitReportServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
class ProfitAreaRuleTest
|
||||
{
|
||||
@Test
|
||||
void reportQueriesResolveRuleOnServerAndRejectUnknownAreas()
|
||||
{
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
PrcProfitReportMapper mapper = (PrcProfitReportMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(), new Class<?>[] { PrcProfitReportMapper.class },
|
||||
(proxy, method, args) -> {
|
||||
calls.incrementAndGet();
|
||||
return Collections.emptyList();
|
||||
});
|
||||
PrcProfitReportServiceImpl service = new PrcProfitReportServiceImpl();
|
||||
ReflectionTestUtils.setField(service, "profitReportMapper", mapper);
|
||||
|
||||
PrcProfitReportQuery query = new PrcProfitReportQuery();
|
||||
query.setCustomerArea(" GZ ");
|
||||
query.setProfitRule("SHENZHEN");
|
||||
service.selectSummary(query);
|
||||
assertEquals("GZ", query.getCustomerArea());
|
||||
assertEquals("GUANGZHOU", query.getProfitRule());
|
||||
|
||||
query.setCustomerArea("C");
|
||||
query.setProfitRule("GUANGZHOU");
|
||||
service.selectOrderRows(query);
|
||||
assertEquals("SHENZHEN", query.getProfitRule());
|
||||
service.selectProductRows(query);
|
||||
assertEquals(3, calls.get());
|
||||
|
||||
query.setCustomerArea("UNKNOWN");
|
||||
assertThrows(ServiceException.class, () -> service.selectProductRows(query));
|
||||
query.setCustomerArea("");
|
||||
assertThrows(ServiceException.class, () -> service.selectSummary(query));
|
||||
assertEquals(3, calls.get());
|
||||
assertEquals("SHENZHEN", service.selectAreaRules().get("C"));
|
||||
assertEquals("GUANGZHOU", service.selectAreaRules().get("GZ"));
|
||||
}
|
||||
}
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
package com.bms.web.controller.system;
|
||||
|
||||
import com.bms.common.constant.CacheConstants;
|
||||
import com.bms.common.constant.Constants;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.core.redis.RedisCache;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.framework.web.service.TokenService;
|
||||
import com.bms.framework.web.service.UserDetailsServiceImpl;
|
||||
import com.bms.framework.config.FastJson2JsonRedisSerializer;
|
||||
import com.bms.system.service.ISysUserService;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class SysImpersonationTest {
|
||||
private final ISysUserService users = mock(ISysUserService.class);
|
||||
private final UserDetailsServiceImpl details = mock(UserDetailsServiceImpl.class);
|
||||
private final TokenService tokens = mock(TokenService.class);
|
||||
private final SysImpersonationController controller = new SysImpersonationController(users, details, tokens);
|
||||
|
||||
@AfterEach
|
||||
void clearSecurityContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlySuperAdministratorCanStart() {
|
||||
authenticate(loginUser(2L, "sales"));
|
||||
assertThrows(ServiceException.class, () -> controller.start(3L));
|
||||
verifyNoInteractions(users, details, tokens);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDisabledAndAdministratorTargets() {
|
||||
authenticate(loginUser(1L, "admin"));
|
||||
SysUser disabled = user(2L, "sales");
|
||||
disabled.setStatus("1");
|
||||
when(users.selectUserById(2L)).thenReturn(disabled);
|
||||
assertThrows(ServiceException.class, () -> controller.start(2L));
|
||||
assertThrows(ServiceException.class, () -> controller.start(1L));
|
||||
verifyNoInteractions(details, tokens);
|
||||
}
|
||||
|
||||
@Test
|
||||
void issuedSessionUsesTargetIdentityAndRetainsAdministrator() {
|
||||
LoginUser admin = loginUser(1L, "admin");
|
||||
admin.setToken("parent-session");
|
||||
authenticate(admin);
|
||||
SysUser target = user(2L, "sales");
|
||||
LoginUser targetLogin = new LoginUser(2L, 10L, target, Collections.singleton("bill:list"));
|
||||
when(users.selectUserById(2L)).thenReturn(target);
|
||||
when(details.createLoginUser(target)).thenReturn(targetLogin);
|
||||
when(tokens.createToken(targetLogin)).thenReturn("target-jwt");
|
||||
long started = System.currentTimeMillis();
|
||||
|
||||
AjaxResult result = controller.start(2L);
|
||||
|
||||
assertEquals("target-jwt", result.get(Constants.TOKEN));
|
||||
assertEquals(2L, targetLogin.getUserId());
|
||||
assertEquals(1L, targetLogin.getImpersonatorId());
|
||||
assertEquals("parent-session", targetLogin.getImpersonatorToken());
|
||||
assertTrue(targetLogin.getImpersonationExpireTime() >= started + 29 * 60_000L);
|
||||
assertTrue(targetLogin.getImpersonationExpireTime() <= System.currentTimeMillis() + 30 * 60_000L);
|
||||
verify(users).checkUserDataScope(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTargetWithAdministratorRole() {
|
||||
authenticate(loginUser(1L, "admin"));
|
||||
SysUser target = user(2L, "other-admin");
|
||||
SysRole role = new SysRole();
|
||||
role.setRoleId(1L);
|
||||
target.setRoles(Collections.singletonList(role));
|
||||
when(users.selectUserById(2L)).thenReturn(target);
|
||||
assertThrows(ServiceException.class, () -> controller.start(2L));
|
||||
verifyNoInteractions(details, tokens);
|
||||
}
|
||||
|
||||
@Test
|
||||
void impersonationContextSurvivesRedisSerialization() {
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setImpersonatorId(1L);
|
||||
acting.setImpersonatorName("admin");
|
||||
acting.setImpersonatorToken("parent");
|
||||
acting.setImpersonationExpireTime(System.currentTimeMillis() + 60_000L);
|
||||
FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
|
||||
LoginUser restored = (LoginUser) serializer.deserialize(serializer.serialize(acting));
|
||||
assertTrue(restored.isImpersonating());
|
||||
assertEquals(1L, restored.getImpersonatorId());
|
||||
assertEquals("parent", restored.getImpersonatorToken());
|
||||
assertEquals(acting.getImpersonationExpireTime(), restored.getImpersonationExpireTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSessionWhenAdministratorSessionWasRevoked() {
|
||||
RedisCache redis = mock(RedisCache.class);
|
||||
TokenService service = tokenService(redis);
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setImpersonatorId(1L);
|
||||
acting.setImpersonatorToken("parent");
|
||||
acting.setImpersonationExpireTime(System.currentTimeMillis() + 60_000L);
|
||||
when(redis.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + "acting")).thenReturn(acting);
|
||||
when(redis.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + "parent")).thenReturn(null);
|
||||
|
||||
assertNull(service.getLoginUser(request("acting")));
|
||||
verify(redis).deleteObject(CacheConstants.LOGIN_TOKEN_KEY + "acting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsExpiredSessionEvenWhenAdministratorIsStillLoggedIn() {
|
||||
RedisCache redis = mock(RedisCache.class);
|
||||
TokenService service = tokenService(redis);
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setImpersonatorId(1L);
|
||||
acting.setImpersonatorToken("parent");
|
||||
acting.setImpersonationExpireTime(System.currentTimeMillis() - 1);
|
||||
when(redis.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + "acting")).thenReturn(acting);
|
||||
when(redis.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + "parent")).thenReturn(loginUser(1L, "admin"));
|
||||
|
||||
assertNull(service.getLoginUser(request("acting")));
|
||||
verify(redis).deleteObject(CacheConstants.LOGIN_TOKEN_KEY + "acting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exitRevokesOnlyTheImpersonationSession() {
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setToken("acting");
|
||||
acting.setImpersonatorId(1L);
|
||||
acting.setImpersonatorToken("parent");
|
||||
authenticate(acting);
|
||||
|
||||
assertEquals(200, controller.exit().get("code"));
|
||||
verify(tokens).delLoginUser("acting");
|
||||
verify(tokens, never()).delLoginUser("parent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshCannotExtendPastAbsoluteDeadline() {
|
||||
RedisCache redis = mock(RedisCache.class);
|
||||
TokenService service = tokenService(redis);
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setToken("acting");
|
||||
acting.setImpersonatorId(1L);
|
||||
long deadline = System.currentTimeMillis() + 45_000L;
|
||||
acting.setImpersonationExpireTime(deadline);
|
||||
|
||||
service.refreshToken(acting);
|
||||
|
||||
assertEquals(deadline, acting.getExpireTime());
|
||||
ArgumentCaptor<Integer> ttl = ArgumentCaptor.forClass(Integer.class);
|
||||
verify(redis).setCacheObject(eq(CacheConstants.LOGIN_TOKEN_KEY + "acting"), same(acting), ttl.capture(), eq(TimeUnit.SECONDS));
|
||||
assertTrue(ttl.getValue() > 0 && ttl.getValue() <= 45);
|
||||
}
|
||||
|
||||
private static final String SECRET = "test-secret-for-impersonation-verification-012345678901234567890123456789";
|
||||
|
||||
private TokenService tokenService(RedisCache redis) {
|
||||
TokenService service = new TokenService();
|
||||
ReflectionTestUtils.setField(service, "redisCache", redis);
|
||||
ReflectionTestUtils.setField(service, "header", "Authorization");
|
||||
ReflectionTestUtils.setField(service, "secret", SECRET);
|
||||
ReflectionTestUtils.setField(service, "expireTime", 1440);
|
||||
return service;
|
||||
}
|
||||
|
||||
private MockHttpServletRequest request(String uuid) {
|
||||
String jwt = Jwts.builder().setClaims(Map.of(Constants.LOGIN_USER_KEY, uuid))
|
||||
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + jwt);
|
||||
return request;
|
||||
}
|
||||
|
||||
private LoginUser loginUser(long id, String name) {
|
||||
SysUser user = user(id, name);
|
||||
return new LoginUser(id, 10L, user, Collections.emptySet());
|
||||
}
|
||||
|
||||
private SysUser user(long id, String name) {
|
||||
SysUser user = new SysUser(id);
|
||||
user.setUserName(name);
|
||||
user.setStatus("0");
|
||||
user.setDelFlag("0");
|
||||
return user;
|
||||
}
|
||||
|
||||
private void authenticate(LoginUser user) {
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));
|
||||
}
|
||||
}
|
||||
@@ -75,24 +75,6 @@ public class LoginUser implements UserDetails
|
||||
*/
|
||||
private SysUser user;
|
||||
|
||||
/** 代登录的实际操作人及其原会话标识,仅保存在服务端登录态中。 */
|
||||
private Long impersonatorId;
|
||||
private String impersonatorName;
|
||||
private String impersonatorToken;
|
||||
private Long impersonationExpireTime;
|
||||
|
||||
public Long getImpersonatorId() { return impersonatorId; }
|
||||
public void setImpersonatorId(Long impersonatorId) { this.impersonatorId = impersonatorId; }
|
||||
public String getImpersonatorName() { return impersonatorName; }
|
||||
public void setImpersonatorName(String impersonatorName) { this.impersonatorName = impersonatorName; }
|
||||
public String getImpersonatorToken() { return impersonatorToken; }
|
||||
public void setImpersonatorToken(String impersonatorToken) { this.impersonatorToken = impersonatorToken; }
|
||||
public Long getImpersonationExpireTime() { return impersonationExpireTime; }
|
||||
public void setImpersonationExpireTime(Long impersonationExpireTime) { this.impersonationExpireTime = impersonationExpireTime; }
|
||||
|
||||
@JSONField(serialize = false)
|
||||
public boolean isImpersonating() { return impersonatorId != null; }
|
||||
|
||||
public LoginUser()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -168,35 +168,6 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
|
||||
return (str == null ? "" : str.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize destination postcode for zone match / persist.
|
||||
* Removes all Unicode whitespace (incl. NBSP / U+2005) and ASCII spaces;
|
||||
* strips Excel-style trailing ".0" on pure digits.
|
||||
*/
|
||||
public static String normalizePostcode(String postcode)
|
||||
{
|
||||
if (isEmpty(postcode))
|
||||
{
|
||||
return postcode;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(postcode.length());
|
||||
for (int i = 0; i < postcode.length(); )
|
||||
{
|
||||
int cp = postcode.codePointAt(i);
|
||||
if (!Character.isWhitespace(cp))
|
||||
{
|
||||
sb.appendCodePoint(cp);
|
||||
}
|
||||
i += Character.charCount(cp);
|
||||
}
|
||||
String n = sb.toString();
|
||||
if (n.endsWith(".0") && n.matches("\\d+\\.0"))
|
||||
{
|
||||
return n.substring(0, n.length() - 2);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换指定字符串的指定区间内字符为"*"
|
||||
*
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
package com.bms.common.utils.poi;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.DateUtil;
|
||||
import org.apache.poi.ss.usermodel.Hyperlink;
|
||||
import org.apache.poi.xssf.usermodel.XSSFHyperlink;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
|
||||
/**
|
||||
* 把一个工作表读成文本网格。
|
||||
* 合并单元格只保留左上角的原文,不把国家、处理费、超重费再填到每一段重量上。
|
||||
*/
|
||||
public class ExcelGrid
|
||||
{
|
||||
/** 单表最多读取的行数,避免样式撑满的空行把内存打满 */
|
||||
public static final int MAX_ROWS = 2500;
|
||||
|
||||
/** 单表最多读取的列数。价卡分区列不会超过这个宽度 */
|
||||
public static final int MAX_COLS = 60;
|
||||
|
||||
private final String[][] text;
|
||||
|
||||
private final Double[][] number;
|
||||
|
||||
private final String[][] linkSheet;
|
||||
|
||||
/** 合并区域左上角横向占几列,其余单元格为 0 */
|
||||
private final int[][] colSpan;
|
||||
|
||||
/** 纵向合并时,被盖住的格子指向左上角行;没有纵向合并是 -1 */
|
||||
private final int[][] rowAnchor;
|
||||
|
||||
private final int rows;
|
||||
|
||||
private final int cols;
|
||||
|
||||
private ExcelGrid(String[][] text, Double[][] number, String[][] linkSheet, int[][] colSpan, int[][] rowAnchor, int rows, int cols)
|
||||
{
|
||||
this.text = text;
|
||||
this.number = number;
|
||||
this.linkSheet = linkSheet;
|
||||
this.colSpan = colSpan;
|
||||
this.rowAnchor = rowAnchor;
|
||||
this.rows = rows;
|
||||
this.cols = cols;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取工作表。日期写成 yyyy-MM-dd,不把日期序列号当成金额。
|
||||
*/
|
||||
public static ExcelGrid read(Sheet sheet)
|
||||
{
|
||||
return read(sheet, MAX_ROWS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按指定行数上限读取。邮编分区表会超过价卡默认的 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);
|
||||
int cols = 0;
|
||||
if (sheet != null)
|
||||
{
|
||||
for (int r = 0; r <= lastRow; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null || row.getLastCellNum() <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int lastCell = Math.min(row.getLastCellNum(), MAX_COLS);
|
||||
for (int c = 0; c < lastCell; c++)
|
||||
{
|
||||
Cell cell = row.getCell(c);
|
||||
if (hasValue(cell) && c + 1 > cols)
|
||||
{
|
||||
cols = c + 1;
|
||||
}
|
||||
}
|
||||
reportRow(progress, 0, r, lastRow + 1);
|
||||
}
|
||||
}
|
||||
int rows = lastRow + 1;
|
||||
String[][] text = new String[Math.max(rows, 0)][Math.max(cols, 0)];
|
||||
Double[][] number = new Double[Math.max(rows, 0)][Math.max(cols, 0)];
|
||||
String[][] linkSheet = new String[Math.max(rows, 0)][Math.max(cols, 0)];
|
||||
int[][] colSpan = new int[Math.max(rows, 0)][Math.max(cols, 0)];
|
||||
int[][] rowAnchor = new int[Math.max(rows, 0)][Math.max(cols, 0)];
|
||||
for (int r = 0; r < rowAnchor.length; r++)
|
||||
{
|
||||
for (int c = 0; c < rowAnchor[r].length; c++)
|
||||
{
|
||||
rowAnchor[r][c] = -1;
|
||||
}
|
||||
}
|
||||
if (sheet != null && rows > 0 && cols > 0)
|
||||
{
|
||||
for (int r = 0; r < rows; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int c = 0; c < cols; c++)
|
||||
{
|
||||
fill(row.getCell(c), text, number, linkSheet, r, c);
|
||||
}
|
||||
reportRow(progress, 1, r, rows);
|
||||
}
|
||||
for (CellRangeAddress region : sheet.getMergedRegions())
|
||||
{
|
||||
int r0 = Math.max(region.getFirstRow(), 0);
|
||||
int c0 = Math.max(region.getFirstColumn(), 0);
|
||||
if (r0 >= rows || c0 >= cols)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int width = region.getLastColumn() - region.getFirstColumn() + 1;
|
||||
if (width > 1)
|
||||
{
|
||||
colSpan[r0][c0] = width;
|
||||
}
|
||||
int bottom = Math.min(region.getLastRow(), rows - 1);
|
||||
int right = Math.min(region.getLastColumn(), cols - 1);
|
||||
if (bottom > r0)
|
||||
{
|
||||
for (int r = r0 + 1; r <= bottom; r++)
|
||||
{
|
||||
for (int c = c0; c <= right; c++)
|
||||
{
|
||||
rowAnchor[r][c] = r0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public int cols()
|
||||
{
|
||||
return cols;
|
||||
}
|
||||
|
||||
public String text(int row, int col)
|
||||
{
|
||||
if (row < 0 || col < 0 || row >= rows || col >= cols)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return text[row][col] == null ? "" : text[row][col];
|
||||
}
|
||||
|
||||
/** 本格有字就用本格。纵向合并盖住的空格,用合并区左上角的原文。 */
|
||||
public String mergedText(int row, int col)
|
||||
{
|
||||
String own = text(row, col);
|
||||
if (!own.isEmpty() || row < 0 || col < 0 || row >= rows || col >= cols)
|
||||
{
|
||||
return own;
|
||||
}
|
||||
int anchor = rowAnchor[row][col];
|
||||
return anchor < 0 ? "" : text(anchor, col);
|
||||
}
|
||||
|
||||
public Double number(int row, int col)
|
||||
{
|
||||
if (row < 0 || col < 0 || row >= rows || col >= cols)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return number[row][col];
|
||||
}
|
||||
|
||||
/** 单元格内部超链接指向的工作表名,没有链接时返回空串 */
|
||||
public String linkSheet(int row, int col)
|
||||
{
|
||||
if (row < 0 || col < 0 || row >= rows || col >= cols || linkSheet[row][col] == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return linkSheet[row][col];
|
||||
}
|
||||
|
||||
/** 该格是横向合并的左上角时,返回合并了几列;普通格返回 1 */
|
||||
public int colSpan(int row, int col)
|
||||
{
|
||||
if (row < 0 || col < 0 || row >= rows || col >= cols)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return colSpan[row][col] > 1 ? colSpan[row][col] : 1;
|
||||
}
|
||||
|
||||
public boolean blankRow(int row)
|
||||
{
|
||||
if (row < 0 || row >= rows)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
for (int c = 0; c < cols; c++)
|
||||
{
|
||||
if (!isBlank(text[row][c]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void fill(Cell cell, String[][] text, Double[][] number, String[][] linkSheet, int r, int c)
|
||||
{
|
||||
if (cell == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
linkSheet[r][c] = sheetName(cell.getHyperlink());
|
||||
CellType type = cell.getCellType();
|
||||
if (type == CellType.FORMULA)
|
||||
{
|
||||
type = cell.getCachedFormulaResultType();
|
||||
}
|
||||
if (type == CellType.NUMERIC)
|
||||
{
|
||||
if (DateUtil.isCellDateFormatted(cell))
|
||||
{
|
||||
Date date = cell.getDateCellValue();
|
||||
text[r][c] = date == null ? "" : DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, date);
|
||||
return;
|
||||
}
|
||||
double raw = cell.getNumericCellValue();
|
||||
number[r][c] = raw;
|
||||
text[r][c] = BigDecimal.valueOf(raw).stripTrailingZeros().toPlainString();
|
||||
return;
|
||||
}
|
||||
if (type == CellType.STRING)
|
||||
{
|
||||
text[r][c] = clean(cell.getStringCellValue());
|
||||
number[r][c] = plainNumber(text[r][c]);
|
||||
return;
|
||||
}
|
||||
if (type == CellType.BOOLEAN)
|
||||
{
|
||||
text[r][c] = Boolean.toString(cell.getBooleanCellValue());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasValue(Cell cell)
|
||||
{
|
||||
if (cell == null || cell.getCellType() == CellType.BLANK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (cell.getCellType() == CellType.STRING)
|
||||
{
|
||||
return !isBlank(cell.getStringCellValue());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Double plainNumber(String text)
|
||||
{
|
||||
if (text == null || !text.matches("-?\\d+(\\.\\d+)?"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return Double.valueOf(text);
|
||||
}
|
||||
catch (NumberFormatException ex)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 内部链接形如 目录!A1 或 'evri 偏远'!A1,这里只留工作表名 */
|
||||
static String sheetName(Hyperlink link)
|
||||
{
|
||||
if (link == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String location = link instanceof XSSFHyperlink ? ((XSSFHyperlink) link).getLocation() : "";
|
||||
if (isBlank(location))
|
||||
{
|
||||
location = link.getAddress();
|
||||
}
|
||||
if (isBlank(location) || !location.contains("!"))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String name = location.substring(0, location.lastIndexOf('!')).trim();
|
||||
if (name.startsWith("'") && name.endsWith("'") && name.length() >= 2)
|
||||
{
|
||||
name = name.substring(1, name.length() - 1).replace("''", "'");
|
||||
}
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
private static String clean(String value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return value.replace('\u00A0', ' ').replace('\r', ' ').trim();
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value)
|
||||
{
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ public class LogAspect
|
||||
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
|
||||
if (loginUser != null)
|
||||
{
|
||||
operLog.setOperName(loginUser.isImpersonating() ? loginUser.getImpersonatorName() : loginUser.getUsername());
|
||||
operLog.setOperName(loginUser.getUsername());
|
||||
SysUser currentUser = loginUser.getUser();
|
||||
if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept()))
|
||||
{
|
||||
@@ -122,11 +122,6 @@ public class LogAspect
|
||||
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
|
||||
// 处理设置注解上的参数
|
||||
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
|
||||
if (loginUser != null && loginUser.isImpersonating())
|
||||
{
|
||||
String target = "代登录目标=" + loginUser.getUsername() + "(ID=" + loginUser.getUserId() + "); ";
|
||||
operLog.setOperParam(StringUtils.substring(target + StringUtils.defaultString(operLog.getOperParam()), 0, PARAM_MAX_LENGTH));
|
||||
}
|
||||
// 设置消耗时间
|
||||
operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
|
||||
// 保存数据库
|
||||
|
||||
+1
-48
@@ -11,16 +11,10 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.framework.web.service.TokenService;
|
||||
import com.bms.framework.manager.AsyncManager;
|
||||
import com.bms.framework.manager.factory.AsyncFactory;
|
||||
import com.bms.system.domain.SysOperLog;
|
||||
import com.bms.common.utils.ip.IpUtils;
|
||||
|
||||
/**
|
||||
* token过滤器 验证token有效性
|
||||
@@ -30,7 +24,6 @@ import com.bms.common.utils.ip.IpUtils;
|
||||
@Component
|
||||
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
|
||||
@@ -46,46 +39,6 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
|
||||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||
}
|
||||
if (loginUser == null || !loginUser.isImpersonating())
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
long started = System.currentTimeMillis();
|
||||
boolean failed = false;
|
||||
try
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
catch (IOException | ServletException | RuntimeException e)
|
||||
{
|
||||
failed = true;
|
||||
throw e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 记录所有代登录请求的真实操作人和目标账号,不记录令牌、请求参数或响应正文。
|
||||
try
|
||||
{
|
||||
SysOperLog accessLog = new SysOperLog();
|
||||
accessLog.setTitle("代登录访问");
|
||||
accessLog.setBusinessType(0);
|
||||
accessLog.setOperatorType(1);
|
||||
accessLog.setOperName(loginUser.getImpersonatorName());
|
||||
accessLog.setOperIp(IpUtils.getIpAddr(request));
|
||||
accessLog.setOperUrl(StringUtils.substring(request.getRequestURI(), 0, 255));
|
||||
accessLog.setRequestMethod(request.getMethod());
|
||||
accessLog.setMethod("impersonation");
|
||||
accessLog.setOperParam("实际管理员ID=" + loginUser.getImpersonatorId() + ", 目标用户="
|
||||
+ loginUser.getUsername() + "(ID=" + loginUser.getUserId() + "), HTTP状态=" + response.getStatus());
|
||||
accessLog.setStatus(failed || response.getStatus() >= 400 ? 1 : 0);
|
||||
accessLog.setCostTime(System.currentTimeMillis() - started);
|
||||
AsyncManager.me().execute(AsyncFactory.recordOper(accessLog));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("记录代登录访问日志失败", e);
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-34
@@ -74,19 +74,6 @@ public class TokenService
|
||||
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
||||
String userKey = getTokenKey(uuid);
|
||||
LoginUser user = redisCache.getCacheObject(userKey);
|
||||
if (user != null && user.isImpersonating())
|
||||
{
|
||||
LoginUser operator = StringUtils.isEmpty(user.getImpersonatorToken()) ? null
|
||||
: redisCache.getCacheObject(getTokenKey(user.getImpersonatorToken()));
|
||||
if (user.getImpersonationExpireTime() == null
|
||||
|| user.getImpersonationExpireTime() <= System.currentTimeMillis()
|
||||
|| operator == null || operator.isImpersonating()
|
||||
|| !user.getImpersonatorId().equals(operator.getUserId()))
|
||||
{
|
||||
redisCache.deleteObject(userKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return user;
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -163,29 +150,10 @@ public class TokenService
|
||||
public void refreshToken(LoginUser loginUser)
|
||||
{
|
||||
loginUser.setLoginTime(System.currentTimeMillis());
|
||||
long expiry = loginUser.getLoginTime() + expireTime * MILLIS_MINUTE;
|
||||
if (loginUser.isImpersonating())
|
||||
{
|
||||
if (loginUser.getImpersonationExpireTime() == null
|
||||
|| loginUser.getImpersonationExpireTime() <= loginUser.getLoginTime())
|
||||
{
|
||||
delLoginUser(loginUser.getToken());
|
||||
return;
|
||||
}
|
||||
expiry = Math.min(expiry, loginUser.getImpersonationExpireTime());
|
||||
}
|
||||
loginUser.setExpireTime(expiry);
|
||||
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
|
||||
// 根据uuid将loginUser缓存
|
||||
String userKey = getTokenKey(loginUser.getToken());
|
||||
if (loginUser.isImpersonating())
|
||||
{
|
||||
int ttlSeconds = (int) Math.max(1, (expiry - loginUser.getLoginTime() + 999) / 1000);
|
||||
redisCache.setCacheObject(userKey, loginUser, ttlSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
else
|
||||
{
|
||||
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
|
||||
}
|
||||
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,13 +36,6 @@ public class ArBill extends BaseEntity {
|
||||
private Long customerId;
|
||||
private String customerCode;
|
||||
private String customerName;
|
||||
/** 创建账单时的客户区域和负责人快照。 */
|
||||
private String profitCustomerArea;
|
||||
private Long profitSalesUserId;
|
||||
private String profitSalesUserCode;
|
||||
private String profitSalesUserName;
|
||||
/** NEW=生成时固化;BACKFILLED=历史补录,来源为补录时可取得的数据。 */
|
||||
private String profitSnapshotSource;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
/** 账单所属期开始日期,导出模板展示用。 */
|
||||
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
package com.bms.ar.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import com.bms.common.annotation.Excel;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
import lombok.Data;
|
||||
|
||||
/** 应收账单生成预览结果 Excel 导出行。 */
|
||||
@Data
|
||||
public class ArBillGeneratePreviewExportRow implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "订单号", sort = 0, width = 24)
|
||||
private String orderNo;
|
||||
|
||||
@Excel(name = "运单号", sort = 1, width = 22)
|
||||
private String trackingNo;
|
||||
|
||||
@Excel(name = "转单号", sort = 2, width = 24)
|
||||
private String transferNo;
|
||||
|
||||
@Excel(name = "客户", sort = 3, width = 24)
|
||||
private String customerName;
|
||||
|
||||
@Excel(name = "计费时间", sort = 4, dateFormat = "yyyy-MM-dd HH:mm:ss", width = 22)
|
||||
private Date bizDate;
|
||||
|
||||
@Excel(name = "收货时间", sort = 4, dateFormat = "yyyy-MM-dd HH:mm:ss", width = 22)
|
||||
private Date pickupDate;
|
||||
|
||||
@Excel(name = "产品", sort = 5, width = 22)
|
||||
private String productName;
|
||||
|
||||
@Excel(name = "国家/地区", sort = 6, width = 18)
|
||||
private String countryName;
|
||||
|
||||
@Excel(name = "实际重(kg)", sort = 7, width = 16)
|
||||
private BigDecimal actualWeight;
|
||||
|
||||
@Excel(name = "计费重(kg)", sort = 8, width = 16)
|
||||
private BigDecimal chargeWeight;
|
||||
|
||||
@Excel(name = "应收金额", sort = 9, scale = 2, width = 16)
|
||||
private BigDecimal incomeTotal;
|
||||
|
||||
@Excel(name = "币种", sort = 10, width = 12)
|
||||
private String currency;
|
||||
|
||||
public static ArBillGeneratePreviewExportRow from(PrcOrderBill bill) {
|
||||
ArBillGeneratePreviewExportRow row = new ArBillGeneratePreviewExportRow();
|
||||
row.setOrderNo(bill.getOrderNo());
|
||||
row.setTrackingNo(bill.getTrackingNo());
|
||||
row.setTransferNo(bill.getTransferNo());
|
||||
row.setCustomerName(bill.getCustomerName());
|
||||
row.setBizDate(bill.getBizDate());
|
||||
row.setPickupDate(bill.getPickupDate());
|
||||
row.setProductName(bill.getProductName());
|
||||
row.setCountryName(bill.getCountryName());
|
||||
row.setActualWeight(bill.getActualWeight());
|
||||
row.setChargeWeight(bill.getChargeWeight());
|
||||
row.setIncomeTotal(bill.getIncomeTotal());
|
||||
row.setCurrency(bill.getCurrency());
|
||||
return row;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.bms.ar.domain.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
import lombok.Data;
|
||||
@@ -14,9 +13,6 @@ public class ArBillQuery extends ArBill
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 多选账单状态;为空时与旧版 status 参数保持兼容并表示不按状态筛选。 */
|
||||
private List<String> statuses;
|
||||
|
||||
/** 账单日期查询起始日。 */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDateStart;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.bms.ar.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.ar.profit.ArCommissionRate;
|
||||
import com.bms.ar.profit.ArWriteoffRate;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.ar.profit.ArCommissionSummary;
|
||||
|
||||
public interface ArCommissionMapper
|
||||
{
|
||||
List<ArCommissionRate> selectRates();
|
||||
ArCommissionRate selectEffective(@Param("salesUserId") Long salesUserId, @Param("at") Date at);
|
||||
int insertRate(ArCommissionRate rate);
|
||||
Long lockSalesUser(@Param("salesUserId") Long salesUserId);
|
||||
int insertWriteoffRate(@Param("writeoffId") Long writeoffId, @Param("salesUserId") Long salesUserId,
|
||||
@Param("rateId") Long rateId, @Param("rate") java.math.BigDecimal rate);
|
||||
int reverseResults(@Param("billId") Long billId, @Param("operator") String operator);
|
||||
List<ArCommissionResult> selectSettlementInputs(@Param("billId") Long billId);
|
||||
int insertResults(@Param("items") List<ArCommissionResult> items, @Param("operator") String operator);
|
||||
List<ArWriteoffRate> selectMissingRates();
|
||||
List<ArWriteoffRate> selectWriteoffRates(@Param("billId") Long billId);
|
||||
Long selectBillIdByWriteoffId(@Param("writeoffId") Long writeoffId);
|
||||
int countMissingRates(@Param("billId") Long billId);
|
||||
int confirmMissingRate(@Param("writeoffId") Long writeoffId, @Param("rate") java.math.BigDecimal rate,
|
||||
@Param("operator") String operator, @Param("remark") String remark);
|
||||
List<ArCommissionResult> selectResults(@Param("billId") Long billId, @Param("includeReversed") boolean includeReversed);
|
||||
List<ArCommissionSummary> selectCommissionSummary(@Param("billId") Long billId);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.bms.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.ar.profit.ArProfitQuery;
|
||||
import com.bms.ar.profit.ArProfitRow;
|
||||
import com.bms.ar.profit.ArProfitScope;
|
||||
import com.bms.ar.profit.ArProfitSummary;
|
||||
import com.bms.ar.profit.ArProfitSalesOption;
|
||||
import com.bms.ar.domain.ArBillDetailFee;
|
||||
import com.bms.ar.domain.ArBillDetail;
|
||||
|
||||
public interface ArProfitMapper
|
||||
{
|
||||
List<ArProfitRow> selectBills(@Param("query") ArProfitQuery query, @Param("scope") ArProfitScope scope);
|
||||
List<ArProfitSummary> selectSummary(@Param("query") ArProfitQuery query, @Param("scope") ArProfitScope scope);
|
||||
List<ArProfitSalesOption> selectSalesOptions(@Param("keyword") String keyword);
|
||||
List<ArBillDetail> selectBillDetails(@Param("query") ArProfitQuery query, @Param("scope") ArProfitScope scope);
|
||||
List<ArBillDetailFee> selectFees(@Param("query") ArProfitQuery query, @Param("scope") ArProfitScope scope);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
@Data
|
||||
public class ArCommissionRate
|
||||
{
|
||||
private Long rateId;
|
||||
private Long salesUserId;
|
||||
private String salesUserCode;
|
||||
private String salesUserName;
|
||||
private BigDecimal rate;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date effectiveAt;
|
||||
private String createBy;
|
||||
private String remark;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
import com.bms.common.annotation.Excel;
|
||||
|
||||
@Data
|
||||
public class ArCommissionResult
|
||||
{
|
||||
private Long resultId;
|
||||
private Long arBillId;
|
||||
private Long detailId;
|
||||
private Long salesUserId;
|
||||
@Excel(name = "账单号", width = 20) private String billNo;
|
||||
@Excel(name = "订单号", width = 22) private String orderNo;
|
||||
@Excel(name = "销售员", width = 16) private String salesUserName;
|
||||
@Excel(name = "币种", width = 10) private String currency;
|
||||
@Excel(name = "销售毛利", scale = 4, width = 16) private BigDecimal grossProfit;
|
||||
@Excel(name = "正式提成", scale = 2, width = 16) private BigDecimal commissionAmount;
|
||||
@Excel(name = "状态", readConverterExp = "READY=已计算,MISSING_RATE=比例待确认,MISSING_PRICE=价格缺失,MISSING_SNAPSHOT=归属或区域缺失,WRITEOFF_MISMATCH=核销金额不一致,REVERSED=已冲回", width = 20) private String resultStatus;
|
||||
private Date createTime;
|
||||
private String reversedBy;
|
||||
private Date reversedTime;
|
||||
private BigDecimal weightedRate;
|
||||
private BigDecimal billAmount;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.bms.ar.domain.ArBalanceWriteoff;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
import com.bms.ar.mapper.ArCommissionMapper;
|
||||
import com.bms.ar.mapper.ArBillMapper;
|
||||
import com.bms.ar.enums.ArBillStatus;
|
||||
import com.bms.ar.enums.ArBillGenerationStatus;
|
||||
import com.bms.ar.security.ArBillRoleKeys;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.system.mapper.SysUserMapper;
|
||||
|
||||
@Service
|
||||
public class ArCommissionService
|
||||
{
|
||||
private final ArCommissionMapper mapper;
|
||||
private final ArProfitScopeResolver scopeResolver;
|
||||
private final SysUserMapper userMapper;
|
||||
private final ArBillMapper billMapper;
|
||||
|
||||
public ArCommissionService(ArCommissionMapper mapper, ArProfitScopeResolver scopeResolver, SysUserMapper userMapper,
|
||||
ArBillMapper billMapper)
|
||||
{
|
||||
this.mapper = mapper;
|
||||
this.scopeResolver = scopeResolver;
|
||||
this.userMapper = userMapper;
|
||||
this.billMapper = billMapper;
|
||||
}
|
||||
|
||||
public List<ArCommissionRate> selectRates()
|
||||
{
|
||||
requireFinance();
|
||||
return mapper.selectRates();
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void addRate(ArCommissionRate rate)
|
||||
{
|
||||
requireFinance();
|
||||
SysUser sales = rate == null || rate.getSalesUserId() == null ? null : userMapper.selectUserById(rate.getSalesUserId());
|
||||
if (sales == null || !"0".equals(sales.getStatus()) || !"0".equals(sales.getDelFlag())
|
||||
|| sales.getRoles() == null || sales.getRoles().stream().noneMatch(role -> role != null
|
||||
&& "0".equals(role.getStatus()) && ArBillRoleKeys.SALES.equals(role.getRoleKey())))
|
||||
throw new ServiceException("请选择有效销售员");
|
||||
if (rate.getRate() == null || rate.getRate().signum() < 0 || rate.getRate().compareTo(new BigDecimal("100")) > 0
|
||||
|| rate.getRate().scale() > 4)
|
||||
throw new ServiceException("提成比例须为 0 至 100,最多四位小数");
|
||||
if (rate.getRemark() != null && rate.getRemark().length() > 500)
|
||||
throw new ServiceException("备注不能超过 500 字");
|
||||
mapper.lockSalesUser(rate.getSalesUserId());
|
||||
Date now = new Date();
|
||||
if (rate.getEffectiveAt() == null) rate.setEffectiveAt(now);
|
||||
if (rate.getEffectiveAt().before(now)) throw new ServiceException("提成比例生效时间不能早于当前时间");
|
||||
rate.setCreateBy(SecurityUtils.getUsername());
|
||||
mapper.insertRate(rate);
|
||||
}
|
||||
|
||||
/** 在核销事务内固化该次操作时的比例;未配置时保留空值,待财务处理。 */
|
||||
public void snapshotWriteoff(ArBill bill, ArBalanceWriteoff writeoff)
|
||||
{
|
||||
Long salesUserId = bill.getProfitSalesUserId();
|
||||
if (salesUserId != null) mapper.lockSalesUser(salesUserId);
|
||||
ArCommissionRate rate = salesUserId == null ? null : mapper.selectEffective(salesUserId, new Date());
|
||||
mapper.insertWriteoffRate(writeoff.getWriteoffId(), salesUserId,
|
||||
rate == null ? null : rate.getRateId(), rate == null ? null : rate.getRate());
|
||||
}
|
||||
|
||||
/** 完全核销后生成逐单结果;由外部账单锁及核销事务保证并发一致。 */
|
||||
public void createFinalResults(Long billId, String operator)
|
||||
{
|
||||
List<ArCommissionResult> items = mapper.selectSettlementInputs(billId);
|
||||
CommissionCalculator.allocateFinalAmounts(items);
|
||||
for (int offset = 0; offset < items.size(); offset += 300)
|
||||
{
|
||||
mapper.insertResults(items.subList(offset, Math.min(offset + 300, items.size())), operator);
|
||||
}
|
||||
}
|
||||
|
||||
/** 反核销保留原记录并标记冲回。 */
|
||||
public void reverseFinalResults(Long billId, String operator)
|
||||
{
|
||||
mapper.reverseResults(billId, operator);
|
||||
}
|
||||
|
||||
public List<ArWriteoffRate> missingRates()
|
||||
{
|
||||
requireFinance();
|
||||
return mapper.selectMissingRates();
|
||||
}
|
||||
|
||||
public List<ArCommissionResult> results(Long billId, boolean includeReversed)
|
||||
{
|
||||
requireFinance();
|
||||
return mapper.selectResults(billId, includeReversed);
|
||||
}
|
||||
|
||||
public List<ArCommissionSummary> summary(Long billId)
|
||||
{
|
||||
requireFinance();
|
||||
return mapper.selectCommissionSummary(billId);
|
||||
}
|
||||
|
||||
public List<ArWriteoffRate> writeoffRates(Long billId)
|
||||
{
|
||||
requireFinance();
|
||||
return mapper.selectWriteoffRates(billId);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void confirmMissingRate(ArWriteoffRate input)
|
||||
{
|
||||
requireFinance();
|
||||
if (input == null || input.getWriteoffId() == null || input.getRate() == null
|
||||
|| input.getRate().signum() < 0 || input.getRate().compareTo(new BigDecimal("100")) > 0
|
||||
|| input.getRate().scale() > 4 || input.getRemark() == null || input.getRemark().isBlank()
|
||||
|| input.getRemark().length() > 500)
|
||||
throw new ServiceException("请填写 0 至 100 的提成比例及核对说明");
|
||||
Long billId = mapper.selectBillIdByWriteoffId(input.getWriteoffId());
|
||||
ArBill bill = billId == null ? null : billMapper.selectArBillByIdForUpdate(billId);
|
||||
if (bill == null || bill.getProfitSalesUserId() == null)
|
||||
throw new ServiceException("核销记录无效或账单销售员快照缺失");
|
||||
if (!ArBillGenerationStatus.READY.equals(bill.getGenerationStatus())
|
||||
|| !(ArBillStatus.CONFIRMED.getCode().equals(bill.getStatus())
|
||||
|| ArBillStatus.PARTIAL_WRITTEN_OFF.getCode().equals(bill.getStatus())
|
||||
|| ArBillStatus.WRITTEN_OFF.getCode().equals(bill.getStatus())))
|
||||
throw new ServiceException("账单未生成完成或状态不允许确认提成比例");
|
||||
String operator = SecurityUtils.getUsername();
|
||||
if (mapper.confirmMissingRate(input.getWriteoffId(), input.getRate(), operator, input.getRemark().trim()) != 1)
|
||||
throw new ServiceException("比例已确认或核销状态已变化,请刷新");
|
||||
if (ArBillStatus.WRITTEN_OFF.getCode().equals(bill.getStatus()) && mapper.countMissingRates(billId) == 0)
|
||||
{
|
||||
mapper.reverseResults(billId, operator);
|
||||
createFinalResults(billId, operator);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireFinance()
|
||||
{
|
||||
if (!scopeResolver.resolve().all()) throw new ServiceException("仅财务和系统管理员可查看或维护提成");
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ArCommissionSummary
|
||||
{
|
||||
private Long salesUserId;
|
||||
private String salesUserName;
|
||||
private String currency;
|
||||
private Long orderCount;
|
||||
private Long pendingCount;
|
||||
private BigDecimal commissionAmount;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
@Data
|
||||
public class ArProfitQuery
|
||||
{
|
||||
private Long customerId;
|
||||
private String billNo;
|
||||
private String orderNo;
|
||||
private String status;
|
||||
private Long salesUserId;
|
||||
private Long arBillId;
|
||||
private Long detailId;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date beginBillDate;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date endBillDate;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.bms.common.annotation.Excel;
|
||||
|
||||
@Data
|
||||
public class ArProfitRow
|
||||
{
|
||||
private Long arBillId;
|
||||
@Excel(name = "账单号", width = 20) private String billNo;
|
||||
@Excel(name = "客户代码", width = 16) private String customerCode;
|
||||
@Excel(name = "客户名称", width = 24) private String customerName;
|
||||
private String customerArea;
|
||||
@Excel(name = "归属来源", readConverterExp = "NEW=生成时快照,BACKFILLED=历史补录", width = 14) private String snapshotSource;
|
||||
@Excel(name = "销售员", width = 16) private String salesUserName;
|
||||
@Excel(name = "币种", width = 10) private String currency;
|
||||
@Excel(name = "账单状态", readConverterExp = "CONFIRMED=已确认,PARTIAL_WRITTEN_OFF=部分核销,WRITTEN_OFF=已核销", width = 14) private String status;
|
||||
@Excel(name = "账单日期", dateFormat = "yyyy-MM-dd", width = 15)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDate;
|
||||
@Excel(name = "销售价", scale = 4, width = 16) private BigDecimal salesAmount;
|
||||
@Excel(name = "产品价", scale = 4, width = 16) private BigDecimal productAmount;
|
||||
@Excel(name = "销售毛利", scale = 4, width = 16) private BigDecimal salesGrossProfit;
|
||||
@Excel(name = "销售毛利率(%)", scale = 4, width = 18) private BigDecimal salesGrossMarginRate;
|
||||
@Excel(name = "账单已核销", scale = 2, width = 16) private BigDecimal writtenOffAmount;
|
||||
@Excel(name = "账单未核销", scale = 2, width = 16) private BigDecimal unwrittenAmount;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ArProfitSalesOption
|
||||
{
|
||||
private Long userId;
|
||||
private String name;
|
||||
private String code;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
/** 后端构造的数据范围,不接收前端传入的权限参数。 */
|
||||
public record ArProfitScope(boolean all, boolean self, boolean department, Long userId, Long deptId)
|
||||
{
|
||||
public boolean isAll() { return all; }
|
||||
public boolean isSelf() { return self; }
|
||||
public boolean isDepartment() { return department; }
|
||||
public Long getUserId() { return userId; }
|
||||
public Long getDeptId() { return deptId; }
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.bms.ar.security.ArBillRoleKeys;
|
||||
import com.bms.common.constant.Constants;
|
||||
import com.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.PermissionContext;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
|
||||
@Component
|
||||
public class ArProfitScopeResolver
|
||||
{
|
||||
public ArProfitScope resolve()
|
||||
{
|
||||
return resolve(SecurityUtils.getLoginUser(), PermissionContext.get());
|
||||
}
|
||||
|
||||
public ArProfitScope resolve(LoginUser actor, String permission)
|
||||
{
|
||||
if (actor == null || actor.getUser() == null || actor.getUserId() == null)
|
||||
{
|
||||
throw new ServiceException("登录状态无效,请重新登录", 401);
|
||||
}
|
||||
SysUser user = actor.getUser();
|
||||
if (!"0".equals(user.getStatus()) || !"0".equals(user.getDelFlag()))
|
||||
{
|
||||
return new ArProfitScope(false, false, false, actor.getUserId(), actor.getDeptId());
|
||||
}
|
||||
if (user.isAdmin()) return new ArProfitScope(true, false, false, actor.getUserId(), actor.getDeptId());
|
||||
boolean all = false, self = false, department = false;
|
||||
if (permission != null && !permission.isBlank() && user.getRoles() != null)
|
||||
{
|
||||
for (SysRole role : user.getRoles())
|
||||
{
|
||||
if (role == null || !"0".equals(role.getStatus()) || !permits(role.getPermissions(), permission)) continue;
|
||||
String key = role.getRoleKey();
|
||||
all |= ArBillRoleKeys.ALL_DATA_ROLES.contains(key == null ? "" : key);
|
||||
self |= ArBillRoleKeys.SALES.equals(key);
|
||||
department |= ArBillRoleKeys.SALES_MANAGER.equals(key);
|
||||
}
|
||||
}
|
||||
if (!all && !self && !department) throw new ServiceException("无权访问应收账单毛利分析");
|
||||
return new ArProfitScope(all, self && actor.getUserId() > 0,
|
||||
department && actor.getDeptId() != null && actor.getDeptId() > 0,
|
||||
actor.getUserId(), actor.getDeptId());
|
||||
}
|
||||
|
||||
private boolean permits(Set<String> permissions, String required)
|
||||
{
|
||||
return permissions != null && (permissions.contains(Constants.ALL_PERMISSION)
|
||||
|| Arrays.stream(required.split(Constants.PERMISSION_DELIMITER)).map(String::trim).anyMatch(permissions::contains));
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.bms.ar.mapper.ArProfitMapper;
|
||||
import com.bms.ar.domain.ArBillDetailFee;
|
||||
import com.bms.ar.domain.ArBillDetail;
|
||||
|
||||
@Service
|
||||
public class ArProfitService
|
||||
{
|
||||
private final ArProfitMapper mapper;
|
||||
private final ArProfitScopeResolver scopeResolver;
|
||||
|
||||
public ArProfitService(ArProfitMapper mapper, ArProfitScopeResolver scopeResolver)
|
||||
{
|
||||
this.mapper = mapper;
|
||||
this.scopeResolver = scopeResolver;
|
||||
}
|
||||
|
||||
public List<ArProfitRow> selectBills(ArProfitQuery query)
|
||||
{
|
||||
return mapper.selectBills(query == null ? new ArProfitQuery() : query, scopeResolver.resolve());
|
||||
}
|
||||
|
||||
public List<ArProfitSummary> selectSummary(ArProfitQuery query)
|
||||
{
|
||||
return mapper.selectSummary(query == null ? new ArProfitQuery() : query, scopeResolver.resolve());
|
||||
}
|
||||
|
||||
public List<ArProfitSalesOption> selectSalesOptions(String keyword)
|
||||
{
|
||||
ArProfitScope scope = scopeResolver.resolve();
|
||||
if (!scope.all() && !scope.self() && !scope.department())
|
||||
{
|
||||
return List.of();
|
||||
}
|
||||
if (keyword != null)
|
||||
{
|
||||
keyword = keyword.trim();
|
||||
if (keyword.length() > 100) keyword = keyword.substring(0, 100);
|
||||
}
|
||||
return mapper.selectSalesOptions(keyword);
|
||||
}
|
||||
|
||||
public List<ArBillDetailFee> selectFees(Long detailId)
|
||||
{
|
||||
ArProfitQuery query = new ArProfitQuery();
|
||||
query.setDetailId(detailId);
|
||||
return mapper.selectFees(query, scopeResolver.resolve());
|
||||
}
|
||||
|
||||
public List<ArBillDetail> selectBillDetails(Long billId)
|
||||
{
|
||||
ArProfitQuery query = new ArProfitQuery();
|
||||
query.setArBillId(billId);
|
||||
return mapper.selectBillDetails(query, scopeResolver.resolve());
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ArProfitSummary
|
||||
{
|
||||
private String currency;
|
||||
private Long billCount;
|
||||
private Long invalidBillCount;
|
||||
private BigDecimal salesAmount;
|
||||
private BigDecimal productAmount;
|
||||
private BigDecimal salesGrossProfit;
|
||||
private BigDecimal salesGrossMarginRate;
|
||||
private BigDecimal writtenOffAmount;
|
||||
private BigDecimal unwrittenAmount;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ArWriteoffRate
|
||||
{
|
||||
private Long writeoffId;
|
||||
private Long arBillId;
|
||||
private String billNo;
|
||||
private String customerName;
|
||||
private Long salesUserId;
|
||||
private String salesUserName;
|
||||
private BigDecimal writeoffAmount;
|
||||
private Date writeoffDate;
|
||||
private BigDecimal rate;
|
||||
private String remark;
|
||||
private String status;
|
||||
private String confirmedBy;
|
||||
private Date confirmedTime;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package com.bms.ar.profit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
|
||||
/** 根据账单有效核销及当时比例计算单个账单订单的提成。 */
|
||||
public final class CommissionCalculator
|
||||
{
|
||||
private static final BigDecimal HUNDRED = new BigDecimal("100");
|
||||
|
||||
private CommissionCalculator() {}
|
||||
|
||||
/** 比例以百分数表示,10 代表 10%;缺失比例不得按零处理。 */
|
||||
public record Writeoff(BigDecimal amount, BigDecimal rate) {}
|
||||
|
||||
public record Result(BigDecimal estimated, BigDecimal formal, boolean complete) {}
|
||||
|
||||
public static Result calculate(BigDecimal billAmount, BigDecimal orderProfit, List<Writeoff> writeoffs)
|
||||
{
|
||||
if (billAmount == null || billAmount.signum() <= 0 || orderProfit == null || writeoffs == null)
|
||||
{
|
||||
return new Result(null, null, false);
|
||||
}
|
||||
BigDecimal written = BigDecimal.ZERO;
|
||||
BigDecimal weightedRates = BigDecimal.ZERO;
|
||||
boolean hasMissingRate = false;
|
||||
for (Writeoff writeoff : writeoffs)
|
||||
{
|
||||
if (writeoff == null || writeoff.amount() == null || writeoff.amount().signum() <= 0)
|
||||
{
|
||||
throw new IllegalArgumentException("核销金额必须大于零");
|
||||
}
|
||||
written = written.add(writeoff.amount());
|
||||
if (writeoff.rate() == null)
|
||||
{
|
||||
hasMissingRate = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (writeoff.rate().signum() < 0 || writeoff.rate().compareTo(HUNDRED) > 0)
|
||||
{
|
||||
throw new IllegalArgumentException("提成比例必须在 0 至 100 之间");
|
||||
}
|
||||
weightedRates = weightedRates.add(writeoff.amount().multiply(writeoff.rate()));
|
||||
}
|
||||
}
|
||||
if (written.compareTo(billAmount) > 0)
|
||||
{
|
||||
throw new IllegalArgumentException("有效核销金额超过账单金额");
|
||||
}
|
||||
boolean complete = written.compareTo(billAmount) == 0;
|
||||
if (hasMissingRate)
|
||||
{
|
||||
return new Result(null, null, complete);
|
||||
}
|
||||
BigDecimal amount = orderProfit.multiply(weightedRates)
|
||||
.divide(billAmount.multiply(HUNDRED), 2, RoundingMode.HALF_UP);
|
||||
return new Result(amount, complete ? amount : null, complete);
|
||||
}
|
||||
|
||||
/** 对同一账单的结果按明细顺序分配舍入尾差,使逐单金额之和等于该账单精确总额的四舍五入。 */
|
||||
public static void allocateFinalAmounts(List<ArCommissionResult> items)
|
||||
{
|
||||
BigDecimal cumulativeNumerator = BigDecimal.ZERO;
|
||||
BigDecimal allocated = BigDecimal.ZERO;
|
||||
BigDecimal billAmount = null;
|
||||
BigDecimal weightedRate = null;
|
||||
for (ArCommissionResult item : items)
|
||||
{
|
||||
if (!"READY".equals(item.getResultStatus())) continue;
|
||||
if (item.getBillAmount() == null || item.getBillAmount().signum() <= 0
|
||||
|| item.getGrossProfit() == null || item.getWeightedRate() == null)
|
||||
throw new IllegalArgumentException("正式提成计算数据不完整");
|
||||
if (billAmount == null)
|
||||
{
|
||||
billAmount = item.getBillAmount();
|
||||
weightedRate = item.getWeightedRate();
|
||||
}
|
||||
else if (billAmount.compareTo(item.getBillAmount()) != 0 || weightedRate.compareTo(item.getWeightedRate()) != 0)
|
||||
throw new IllegalArgumentException("同一账单的提成计算依据不一致");
|
||||
cumulativeNumerator = cumulativeNumerator.add(item.getGrossProfit().multiply(item.getWeightedRate()));
|
||||
BigDecimal cumulativeAmount = cumulativeNumerator.divide(billAmount.multiply(HUNDRED), 2, RoundingMode.HALF_UP);
|
||||
item.setCommissionAmount(cumulativeAmount.subtract(allocated));
|
||||
allocated = cumulativeAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
-9
@@ -24,7 +24,6 @@ import com.bms.ar.enums.ArLogResult;
|
||||
import com.bms.ar.mapper.ArBalanceWriteoffMapper;
|
||||
import com.bms.ar.mapper.ArBillMapper;
|
||||
import com.bms.ar.security.ArBillScopeResolver;
|
||||
import com.bms.ar.profit.ArCommissionService;
|
||||
import com.bms.ar.service.IArBalanceWriteoffService;
|
||||
import com.bms.ar.service.IArBillLogService;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
@@ -50,8 +49,6 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
private ArBillMapper billMapper;
|
||||
@Autowired
|
||||
private ArBillScopeResolver billScope;
|
||||
@Autowired
|
||||
private ArCommissionService commissionService;
|
||||
|
||||
@Autowired
|
||||
private ArBalanceWriteoffMapper writeoffMapper;
|
||||
@@ -144,7 +141,6 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
writeoff.setCreateUserId(operatorUserId);
|
||||
writeoff.setCreateBy(operator);
|
||||
writeoffMapper.insertArBalanceWriteoff(writeoff);
|
||||
commissionService.snapshotWriteoff(bill, writeoff);
|
||||
|
||||
// 3. 账单核销口径变更(2026-09-23):核销不再变动账户余额与已用额度,
|
||||
// 仅更新账单已核销/未核销金额与状态,资金变动由收款/计费入口产生。
|
||||
@@ -157,10 +153,6 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
if (ArBillStatus.WRITTEN_OFF.getCode().equals(nextStatus)) {
|
||||
commissionService.createFinalResults(bill.getArBillId(), operator);
|
||||
}
|
||||
|
||||
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), ArLogOperationType.BALANCE_WRITEOFF,
|
||||
"余额核销账单 " + amount.toPlainString() + " " + currency,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
@@ -215,7 +207,6 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
ArBalanceWriteoffStatus.CANCELLED.getCode(), ArBalanceWriteoffStatus.EFFECTIVE.getCode()) != 1) {
|
||||
throw new ServiceException("核销记录状态已变化,请刷新后重试");
|
||||
}
|
||||
commissionService.reverseFinalResults(bill.getArBillId(), operator);
|
||||
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
||||
"反核销取消核销:" + cancelRemark,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
|
||||
@@ -73,10 +73,6 @@ import com.bms.ar.mapper.ArBillLogMapper;
|
||||
import com.bms.ar.mapper.ArBillMapper;
|
||||
import com.bms.ar.security.ArBillScopeResolver;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.system.mapper.SysUserMapper;
|
||||
import com.bms.ar.mapper.ArBillGenerateTaskMapper;
|
||||
import com.bms.ar.mapper.ArBankAccountMapper;
|
||||
import com.bms.ar.mapper.ArExportTemplateMapper;
|
||||
@@ -189,10 +185,6 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Autowired
|
||||
private ArBillMapper billMapper;
|
||||
@Autowired
|
||||
private BmsCustomerMapper customerMapper;
|
||||
@Autowired
|
||||
private SysUserMapper sysUserMapper;
|
||||
@Autowired
|
||||
private ArBillScopeResolver billScope;
|
||||
@Autowired
|
||||
private ArBillGenerateTaskMapper generateTaskMapper;
|
||||
@@ -449,21 +441,6 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
String customerName = StringUtils.trim(request.getCustomerName());
|
||||
bill.setCustomerCode(customerCode == null ? "" : customerCode);
|
||||
bill.setCustomerName(customerName == null ? "" : customerName);
|
||||
bill.setProfitSnapshotSource("NEW");
|
||||
// 固定账单生成时的客户归属;无法确定的用户保持为空,由财务核对。
|
||||
BmsCustomer snapshotCustomer = customerMapper.selectBmsCustomerByCustomerId(request.getCustomerId());
|
||||
if (snapshotCustomer != null) {
|
||||
bill.setProfitCustomerArea(snapshotCustomer.getCustomerArea());
|
||||
String salesId = snapshotCustomer.getSalesUserId();
|
||||
if (salesId != null && salesId.matches("[0-9]{1,18}")) {
|
||||
SysUser salesUser = sysUserMapper.selectUserById(Long.valueOf(salesId));
|
||||
if (salesUser != null) {
|
||||
bill.setProfitSalesUserId(salesUser.getUserId());
|
||||
bill.setProfitSalesUserCode(salesUser.getUserName());
|
||||
bill.setProfitSalesUserName(salesUser.getNickName());
|
||||
}
|
||||
}
|
||||
}
|
||||
bill.setBillPeriodStart(request.getBillPeriodEnd());
|
||||
bill.setBillPeriodEnd(request.getBillPeriodEnd());
|
||||
bill.setBillDate(request.getBillDate() == null ? now : request.getBillDate());
|
||||
@@ -1756,7 +1733,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
case CATALOG_COUNTRY -> "产品价未匹配国家";
|
||||
case CATALOG_WEIGHT -> "产品价未匹配重量段";
|
||||
case NO_CUSTOMER_PACKAGE -> "未绑定套餐";
|
||||
case NO_ACTIVE_PACKAGE_VERSION -> "套餐未启用/无生效版本";
|
||||
case NO_ACTIVE_PACKAGE_VERSION -> "无生效套餐版本";
|
||||
case ALREADY_IN_AR_BILL -> "已生成应收账单";
|
||||
case PRODUCT_ARCHIVE_MISSING -> "产品档案缺失";
|
||||
case PRICE_NOT_HIT -> "未命中价格";
|
||||
|
||||
+3
-3
@@ -47,9 +47,9 @@ public class BmsCustomer extends BaseEntity
|
||||
@Excel(name = "业务类型", readConverterExp = "0=直客,1=同行,2=集运")
|
||||
private String customerType;
|
||||
|
||||
/** 客户区域(字典 customer_area) */
|
||||
@Excel(name = "客户区域", dictType = "customer_area")
|
||||
private String customerArea;
|
||||
/** 区域类型(字典 data_area_type) */
|
||||
@Excel(name = "区域类型", dictType = "data_area_type")
|
||||
private String areaType;
|
||||
|
||||
/** 启用状态(0正常 1停用) */
|
||||
@Excel(name = "启用状态", readConverterExp = "0=正常,1=停用")
|
||||
|
||||
+7
-12
@@ -45,10 +45,10 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FMT =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
|
||||
|
||||
/** 客户区域走字典 customer_area;缓存未命中时保留原编码。 */
|
||||
private static String customerAreaLabel(String value)
|
||||
/** 区域类型走字典 data_area_type;缓存未命中时保留原编码。 */
|
||||
private static String areaTypeLabel(String value)
|
||||
{
|
||||
String label = DictUtils.getDictLabel("customer_area", value);
|
||||
String label = DictUtils.getDictLabel("data_area_type", value);
|
||||
return StringUtils.isEmpty(label) ? value : label;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
/**
|
||||
* 需要记录日志的主表字段(顺序即展示顺序)。
|
||||
* requireNonBlank 为 true 的字段对应 update SQL 的 <if test="x!=null and x!=''">,
|
||||
* 其余动态字段仅在新值非 null 时落库;业务员/结算员/客服、客户区域为无条件写入。
|
||||
* 其余动态字段仅在新值非 null 时落库;业务员/结算员/客服、区域类型为无条件写入。
|
||||
*/
|
||||
private static final List<FieldDef> FIELD_DEFS = List.of(
|
||||
new FieldDef("customerCode", "客户编码", BmsCustomer::getCustomerCode, true, Function.identity()),
|
||||
@@ -85,7 +85,7 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
new FieldDef("customerCodeDw", "对外代码", BmsCustomer::getCustomerCodeDw, false, Function.identity()),
|
||||
new FieldDef("creditCode", "统一社会信用代码", BmsCustomer::getCreditCode, false, Function.identity()),
|
||||
new FieldDef("customerType", "业务类型", BmsCustomer::getCustomerType, false, CUSTOMER_TYPE_MAP::get),
|
||||
new FieldDef("customerArea", "客户区域", BmsCustomer::getCustomerArea, false, BmsCustomerLogServiceImpl::customerAreaLabel),
|
||||
new FieldDef("areaType", "区域类型", BmsCustomer::getAreaType, false, BmsCustomerLogServiceImpl::areaTypeLabel),
|
||||
new FieldDef("isEnable", "启用状态", BmsCustomer::getIsEnable, false, ENABLE_MAP::get),
|
||||
new FieldDef("salesUserId", "业务员", c -> userDisplay(c.getSalesUserId(), c.getSalesUser()), false, Function.identity()),
|
||||
new FieldDef("ed", "资金额度(元)", BmsCustomer::getEd, false, BmsCustomerLogServiceImpl::numText),
|
||||
@@ -208,8 +208,8 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
{
|
||||
// 与 update SQL 的动态条件保持一致:未提交(null/空串)的字段本次不落库,不参与比对
|
||||
Object raw = def.rawGetter.apply(newCustomer);
|
||||
// 合同起止、客户区域允许清空;其余动态字段未提交时不落库,也不记日志
|
||||
if (raw == null && !isContractDateField(def.field) && !"customerArea".equals(def.field))
|
||||
// 合同起止、区域类型允许清空;其余动态字段未提交时不落库,也不记日志
|
||||
if (raw == null && !isContractDateField(def.field) && !"areaType".equals(def.field))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -450,11 +450,6 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService
|
||||
realChanges.add(c);
|
||||
}
|
||||
}
|
||||
// Rebind/update with no field diff: skip empty UPDATE noise (full package save upsert)
|
||||
if (existing != null && realChanges.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
saveLog(bind.getCustomerId(), customer == null ? null : customer.getCustomerCode(),
|
||||
customer == null ? null : customer.getCustomerName(),
|
||||
BmsCustomerLog.MODULE_PRICE_SET, newSet.getPriceSetName(),
|
||||
|
||||
-2
@@ -60,7 +60,6 @@ public class CustomerSyncService
|
||||
private static final int LEN_CODE = 50;
|
||||
private static final int LEN_NAME = 100;
|
||||
private static final int LEN_CODE_DW = 50;
|
||||
private static final int LEN_CUSTOMER_AREA = 32;
|
||||
private static final int LEN_USER = 50;
|
||||
private static final int LEN_REMARK = 500;
|
||||
/** 告警明细最多打印条数 */
|
||||
@@ -179,7 +178,6 @@ public class CustomerSyncService
|
||||
c.setDelFlag("0");
|
||||
|
||||
c.setCustomerCodeDw(truncate(getStr(row, "CUSTOMER_OUTER_CODE"), LEN_CODE_DW, code, "customer_code_dw", warnings));
|
||||
c.setCustomerArea(truncate(getStr(row, "CUSTOMER_AREA"), LEN_CUSTOMER_AREA, code, "customer_area", warnings));
|
||||
String sourceSalesCode = clean(getStr(row, "BELONG_SALER"));
|
||||
c.setSourceSalesCode(sourceSalesCode);
|
||||
c.setSalesUserId(resolveSalesUserId(sourceSalesCode, userIdsByLogin,
|
||||
|
||||
@@ -3,7 +3,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -15,136 +14,72 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -27,11 +27,6 @@ public interface ServiceChannelMapper
|
||||
*/
|
||||
ServiceChannel selectServiceChannelByCode(String channelCode);
|
||||
|
||||
/**
|
||||
* 按渠道名称精确查询,重名时返回多条供调用方拒绝。
|
||||
*/
|
||||
List<ServiceChannel> selectServiceChannelByName(String channelName);
|
||||
|
||||
/**
|
||||
* 产品渠道关系中引用该渠道编码的数量
|
||||
*/
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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
@@ -1,228 +0,0 @@
|
||||
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,40 +0,0 @@
|
||||
package com.bms.price.airbill.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 空运提单费用明细 srv_bill_air_fee。
|
||||
*/
|
||||
@Data
|
||||
public class SrvBillAirFee
|
||||
{
|
||||
private Long id;
|
||||
|
||||
private String billCode;
|
||||
|
||||
/** 字典 bms_fee_type */
|
||||
private String feeType;
|
||||
|
||||
private BigDecimal price;
|
||||
|
||||
/** 字典 bms_currency */
|
||||
private String currency;
|
||||
|
||||
private String remark;
|
||||
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateTime;
|
||||
|
||||
/** 0未删除 1已删除 */
|
||||
private String delFlag;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.bms.price.airbill.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 空运提单费用操作日志。
|
||||
*/
|
||||
@Data
|
||||
public class SrvBillAirFeeLog
|
||||
{
|
||||
public static final String INSERT = "1";
|
||||
|
||||
public static final String UPDATE = "2";
|
||||
|
||||
public static final String DELETE = "3";
|
||||
|
||||
private Long logId;
|
||||
|
||||
private String billCode;
|
||||
|
||||
private Long feeId;
|
||||
|
||||
private String operationType;
|
||||
|
||||
private String summary;
|
||||
|
||||
private String changeContent;
|
||||
|
||||
private String createBy;
|
||||
|
||||
private String createByName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
|
||||
private List<FieldChange> changes;
|
||||
|
||||
@Data
|
||||
public static class FieldChange
|
||||
{
|
||||
private String field;
|
||||
|
||||
private String fieldLabel;
|
||||
|
||||
private String oldValue;
|
||||
|
||||
private String newValue;
|
||||
|
||||
public FieldChange()
|
||||
{
|
||||
}
|
||||
|
||||
public FieldChange(String field, String fieldLabel, String oldValue, String newValue)
|
||||
{
|
||||
this.field = field;
|
||||
this.fieldLabel = fieldLabel;
|
||||
this.oldValue = oldValue;
|
||||
this.newValue = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.bms.price.airbill.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 保存提单费用时只带有变动的行。未出现的费用保持原样。
|
||||
*/
|
||||
@Data
|
||||
public class SrvBillAirFeeSave
|
||||
{
|
||||
private String billCode;
|
||||
|
||||
private List<SrvBillAirFee> inserts = new ArrayList<>();
|
||||
|
||||
private List<SrvBillAirFee> updates = new ArrayList<>();
|
||||
|
||||
private List<Long> deletes = new ArrayList<>();
|
||||
}
|
||||
@@ -20,11 +20,6 @@ public class BillingProperties
|
||||
private int consumerThreads = 8;
|
||||
/** Concurrent threads for monitor order-source sync */
|
||||
private int orderSourceSyncThreads = 8;
|
||||
/**
|
||||
* Concurrent threads for order-list local rebill (2~24).
|
||||
* Keep below Druid maxActive with headroom for other traffic.
|
||||
*/
|
||||
private int localRebillThreads = 16;
|
||||
/** Night window auto-retry of prc_billing_task (all statuses) */
|
||||
private NightRetry nightRetry = new NightRetry();
|
||||
private Oracle oracle = new Oracle();
|
||||
@@ -89,16 +84,6 @@ public class BillingProperties
|
||||
this.orderSourceSyncThreads = orderSourceSyncThreads;
|
||||
}
|
||||
|
||||
public int getLocalRebillThreads()
|
||||
{
|
||||
return localRebillThreads;
|
||||
}
|
||||
|
||||
public void setLocalRebillThreads(int localRebillThreads)
|
||||
{
|
||||
this.localRebillThreads = localRebillThreads;
|
||||
}
|
||||
|
||||
public NightRetry getNightRetry()
|
||||
{
|
||||
return nightRetry;
|
||||
|
||||
@@ -11,7 +11,6 @@ public class BatchAddIncomeBillFeeTask implements Serializable
|
||||
public static final String STATUS_RUNNING = "RUNNING";
|
||||
public static final String STATUS_SUCCESS = "SUCCESS";
|
||||
public static final String STATUS_FAIL = "FAIL";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 渠道和产品的收付关系。payType 取字典 bms_pay_type。
|
||||
*/
|
||||
@Data
|
||||
public class ChannelProductMap
|
||||
{
|
||||
private Long id;
|
||||
|
||||
/** 渠道表主键 srv_service_channel.id */
|
||||
private String channelId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 应收套餐 prc_price_set.price_set_id */
|
||||
private Long priceSetId;
|
||||
|
||||
/** AR 应收,AP 应付 */
|
||||
private String payType;
|
||||
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 应付账单。作废后仍保留。 */
|
||||
@Data
|
||||
public class PayBill
|
||||
{
|
||||
private Long billId;
|
||||
|
||||
private String billNo;
|
||||
|
||||
private Long supplierId;
|
||||
|
||||
private String supplierName;
|
||||
|
||||
private String currency;
|
||||
|
||||
/** ORDER 订单号,TIME 计费日,LADING 提单 */
|
||||
private String sourceType;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date periodStart;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date periodEnd;
|
||||
|
||||
private Integer orderCount;
|
||||
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
/** DRAFT CONFIRMED PAID VOID */
|
||||
private String status;
|
||||
|
||||
private String voidReason;
|
||||
|
||||
private String voidBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date voidTime;
|
||||
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateTime;
|
||||
|
||||
private String remark;
|
||||
|
||||
private List<PayBillLine> lines;
|
||||
|
||||
private List<PayBillLog> logs;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 查询或生成应付账单。订单号和提单号可用逗号、空格、换行分隔。 */
|
||||
@Data
|
||||
public class PayBillCommand
|
||||
{
|
||||
/** 取数方式:ORDER 订单号,TIME 计费日,LADING 提单号 */
|
||||
private String sourceType;
|
||||
|
||||
/** 订单号原文,可用逗号、空格或换行分隔 */
|
||||
private String orderText;
|
||||
|
||||
/** 计费日起,yyyy-MM-dd。sourceType 为 TIME 时必填 */
|
||||
private String beginDate;
|
||||
|
||||
/** 计费日止,yyyy-MM-dd,含当天 */
|
||||
private String endDate;
|
||||
|
||||
/** 提单号原文,可用逗号、空格或换行分隔 */
|
||||
private String ladingText;
|
||||
|
||||
/** 供应商ID。生成账单时必填,预览不用 */
|
||||
private Long supplierId;
|
||||
|
||||
/** 账单备注 */
|
||||
private String remark;
|
||||
|
||||
/** 作废原因。作废时必填 */
|
||||
private String voidReason;
|
||||
|
||||
/** 要作废的应付账单ID */
|
||||
private Long billId;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 应付账单上的一条费用快照。 */
|
||||
@Data
|
||||
public class PayBillLine
|
||||
{
|
||||
private Long lineId;
|
||||
|
||||
private Long billId;
|
||||
|
||||
private String orderBillId;
|
||||
|
||||
private String orderNo;
|
||||
|
||||
private String ladingNo;
|
||||
|
||||
private Long ladingFeeId;
|
||||
|
||||
private String feeName;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
private String currency;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date priceDate;
|
||||
|
||||
private Integer versionNo;
|
||||
|
||||
private String remoteFlag;
|
||||
|
||||
private String densityLevel;
|
||||
|
||||
/** ORDER 订单费用,LADING 留在主单上的费用 */
|
||||
private String lineSource;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 应付账单操作记录。作废后不删除。 */
|
||||
@Data
|
||||
public class PayBillLog
|
||||
{
|
||||
private Long logId;
|
||||
|
||||
private Long billId;
|
||||
|
||||
private String operationType;
|
||||
|
||||
private String fromStatus;
|
||||
|
||||
private String toStatus;
|
||||
|
||||
private String operationDesc;
|
||||
|
||||
private String operatorName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 生成应付账单前查到的一票订单。 */
|
||||
@Data
|
||||
public class PayBillOrder
|
||||
{
|
||||
private String billId;
|
||||
|
||||
private String orderNo;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date bizDate;
|
||||
|
||||
private BigDecimal payTotal;
|
||||
|
||||
private String currency;
|
||||
|
||||
private String ladingNo;
|
||||
|
||||
private String trackingNo;
|
||||
|
||||
private String channelName;
|
||||
|
||||
private String productName;
|
||||
|
||||
private String supplierName;
|
||||
|
||||
private Integer pieces;
|
||||
|
||||
private BigDecimal chargeWeight;
|
||||
|
||||
private String countryCode;
|
||||
|
||||
private String countryName;
|
||||
|
||||
private String destPostcode;
|
||||
|
||||
/** 已算出的应付费用,名称和金额用分号拼在一起 */
|
||||
private String feeText;
|
||||
|
||||
/** 已挂在未作废账单上时的账单号 */
|
||||
private String occupiedBillNo;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/** 应付账单生成前的查询结果。 */
|
||||
@Data
|
||||
public class PayBillPreview
|
||||
{
|
||||
/** 还没进未作废账单、可以生成的订单 */
|
||||
private List<PayBillOrder> orders = new ArrayList<>();
|
||||
|
||||
/** 按订单号查询时没有找到的单号 */
|
||||
private List<String> missingNos = new ArrayList<>();
|
||||
|
||||
/** 已经挂在未作废账单上的订单 */
|
||||
private List<PayBillOrder> occupied = new ArrayList<>();
|
||||
|
||||
/** 给页面的提示,例如未找到订单或超过单次票数 */
|
||||
private String message;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/** 一条算出的应付费用。 */
|
||||
@Data
|
||||
public class PayCostLine
|
||||
{
|
||||
private String feeType;
|
||||
|
||||
private String feeName;
|
||||
|
||||
private Long scId;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
private String currency;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 产品报价表里的邮编或其他不能落进重量段、附加费的配置原文。
|
||||
*/
|
||||
@Data
|
||||
public class PayExtraLine
|
||||
{
|
||||
private Long lineId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 邮编 或 配置 */
|
||||
private String lineType;
|
||||
|
||||
private String lineText;
|
||||
|
||||
private Integer sortNo;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/** 按订单生成应付的结果。skipped 是价卡里有、但这票算不了的附加费。 */
|
||||
@Data
|
||||
public class PayOrderCostResult
|
||||
{
|
||||
private String billId;
|
||||
|
||||
private String channelId;
|
||||
|
||||
private String channelName;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private String productName;
|
||||
|
||||
private Integer versionNo;
|
||||
|
||||
private String currency;
|
||||
|
||||
private BigDecimal payTotal;
|
||||
|
||||
private String message;
|
||||
|
||||
private List<PayCostLine> fees;
|
||||
|
||||
private List<String> skipped;
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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,64 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 应付邮编配置的一条分区或一条合并后的附加费。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PayPostcodeGroup extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long groupId;
|
||||
|
||||
private String sourceFile;
|
||||
|
||||
private String fileMd5;
|
||||
|
||||
private String sheetName;
|
||||
|
||||
private String blockTitle;
|
||||
|
||||
/** ZONE 邮编分区,FEE 附加费 */
|
||||
private String configKind;
|
||||
|
||||
/** REACH 可达,UNREACH 不可达,REMOTE 偏远。普通分区为空 */
|
||||
private String coverage;
|
||||
|
||||
/** 渠道名称,从表头识别,可在页面修改 */
|
||||
private String channelName;
|
||||
|
||||
/** 目录里的服务类型,和报价产品对应 */
|
||||
private String serviceType;
|
||||
|
||||
/** 目录里的国家。邮编目的国仍在 countryName,偏远表里两者可以不同 */
|
||||
private String catalogCountry;
|
||||
|
||||
/** 目录里的销售产品 */
|
||||
private String productName;
|
||||
|
||||
/** 目录里的报价工作表 */
|
||||
private String quoteSheet;
|
||||
|
||||
private String countryName;
|
||||
|
||||
private String countryCode;
|
||||
|
||||
private String feeName;
|
||||
|
||||
private String feeText;
|
||||
|
||||
private Integer lineCount;
|
||||
|
||||
/** 查询条件:邮编 */
|
||||
private String postcode;
|
||||
|
||||
/** 导入时挂上的明细,列表查询不填充 */
|
||||
private List<PayPostcodeLine> lines = new ArrayList<>();
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/** 一次 Excel 导入的汇总,不把全部邮编返回给页面。 */
|
||||
@Data
|
||||
public class PayPostcodeImportResult implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String sourceFile;
|
||||
|
||||
private int groupCount;
|
||||
|
||||
private int feeCount;
|
||||
|
||||
private int zoneCount;
|
||||
|
||||
private int lineCount;
|
||||
|
||||
private String message;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 一条邮编。除末尾星号外,地区、分区和金额都保持 Excel 原文。
|
||||
*/
|
||||
@Data
|
||||
public class PayPostcodeLine implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long lineId;
|
||||
|
||||
private Long groupId;
|
||||
|
||||
private String countryName;
|
||||
|
||||
private String countryCode;
|
||||
|
||||
private String regionName;
|
||||
|
||||
private String regionNameCn;
|
||||
|
||||
private String cityName;
|
||||
|
||||
private String regionCode;
|
||||
|
||||
private String zoneName;
|
||||
|
||||
private String postcode;
|
||||
|
||||
private String postcodeRaw;
|
||||
|
||||
private String postcodeEnd;
|
||||
|
||||
private String extraText;
|
||||
|
||||
private Integer sortNo;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 导入预览。此时还没有写入数据库。
|
||||
*/
|
||||
@Data
|
||||
public class PayPreview
|
||||
{
|
||||
private String sourceFile;
|
||||
|
||||
/** 本次价卡在 OSS 上的地址,保存时写到每个产品版本 */
|
||||
private String filePath;
|
||||
|
||||
private String fileMd5;
|
||||
|
||||
private String supplierName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveDate;
|
||||
|
||||
private List<PayProduct> products = new ArrayList<>();
|
||||
|
||||
/** 没有对上字典、因此没有读取的工作表 */
|
||||
private List<String> skippedSheets = new ArrayList<>();
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/** 旧版和新版价格对照。 */
|
||||
@Data
|
||||
public class PayPriceCompare
|
||||
{
|
||||
private PayProduct older;
|
||||
|
||||
private PayProduct newer;
|
||||
|
||||
private List<PayPriceDiff> weightDiffs;
|
||||
|
||||
private List<PayPriceDiff> surchargeDiffs;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 两个价格版本里的一行差异。 */
|
||||
@Data
|
||||
public class PayPriceDiff
|
||||
{
|
||||
private String name;
|
||||
|
||||
private String zoneName;
|
||||
|
||||
/** 相同、变更、新增、删除。新增和删除相对新版本而言 */
|
||||
private String change;
|
||||
|
||||
private String oldValue;
|
||||
|
||||
private String newValue;
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.bms.common.annotation.Excel;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 应付价卡产品。目录里的一个产品对应一行,重量段和附加费在子表。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PayProduct extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long productId;
|
||||
|
||||
@Excel(name = "产品名称", width = 24)
|
||||
private String productName;
|
||||
|
||||
/** 支付类型,字典 bms_pay_type:AR应收 AP应付 */
|
||||
private String productType;
|
||||
|
||||
/** 字典 payment_product_type 的键值,和报价工作表对应 */
|
||||
private String dictValue;
|
||||
|
||||
/** 复制来源。空表示这是公共模板 */
|
||||
private Long originId;
|
||||
|
||||
/** 服务渠道 ID。空表示公共模板 */
|
||||
private String channelId;
|
||||
|
||||
/** 版本号。模板是 1,渠道另存从 2 起 */
|
||||
private Integer versionNo;
|
||||
|
||||
/** 列表只看公共模板 */
|
||||
private Boolean templateOnly;
|
||||
|
||||
/** 列表每个产品只显示当前生效版;没有生效版时显示最新一版 */
|
||||
private Boolean currentOnly;
|
||||
|
||||
/** 当前价格版本对应的记录。列表上的 productId 是不变的产品编号 */
|
||||
private Long versionProductId;
|
||||
|
||||
/** 当前版本的有效期记录 */
|
||||
private Long termId;
|
||||
|
||||
/** 0 生效,1 失效 */
|
||||
private String termStatus;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date termStart;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date termEnd;
|
||||
|
||||
@Excel(name = "国家/口岸", width = 18)
|
||||
private String countryScope;
|
||||
|
||||
/** 字典 order_charge_type:续重计费、单票价、常规计重价 */
|
||||
@Excel(name = "费用类型", width = 14)
|
||||
private String chargeType;
|
||||
|
||||
/** 续重时每公斤加价。各分区相同是一个金额,不同是最小~最大 */
|
||||
@Excel(name = "每公斤加价", width = 14)
|
||||
private String extraPerKg;
|
||||
|
||||
@Excel(name = "服务类型", width = 16)
|
||||
private String serviceType;
|
||||
|
||||
@Excel(name = "分类", width = 16)
|
||||
private String classification;
|
||||
|
||||
@Excel(name = "目录备注", width = 28)
|
||||
private String catalogRemark;
|
||||
|
||||
@Excel(name = "更新内容", width = 18)
|
||||
private String updateContent;
|
||||
|
||||
@Excel(name = "重量限制", width = 14)
|
||||
private String weightLimit;
|
||||
|
||||
@Excel(name = "报价工作表", width = 24)
|
||||
private String sheetName;
|
||||
|
||||
@Excel(name = "关联分区表", width = 24)
|
||||
private String relatedSheet;
|
||||
|
||||
@Excel(name = "来源文件", width = 36)
|
||||
private String sourceFile;
|
||||
|
||||
/** 源文件在 OSS 上的地址,形如 oss://bucket/key。历史记录按它下载 */
|
||||
private String filePath;
|
||||
|
||||
private String fileMd5;
|
||||
|
||||
@Excel(name = "价卡归属", width = 12)
|
||||
private String supplierName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
@Excel(name = "生效日期", width = 14, dateFormat = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
@Excel(name = "币种", width = 10)
|
||||
private String currency;
|
||||
|
||||
@Excel(name = "服务说明", width = 40)
|
||||
private String serviceNote;
|
||||
|
||||
@Excel(name = "识别结果", width = 12)
|
||||
private String parseStatus;
|
||||
|
||||
@Excel(name = "重量段数", width = 12)
|
||||
private Integer weightCount;
|
||||
|
||||
@Excel(name = "附加费数", width = 12)
|
||||
private Integer surchargeCount;
|
||||
|
||||
@Excel(name = "其他配置数", width = 12)
|
||||
private Integer extraCount;
|
||||
|
||||
/** 查询条件:生效日起,格式 yyyy-MM-dd */
|
||||
private String effectiveBegin;
|
||||
|
||||
/** 查询条件:生效日止 */
|
||||
private String effectiveEnd;
|
||||
|
||||
/** 查询条件:附加费名称、收费标准、费用说明或时效原文 */
|
||||
private String surchargeName;
|
||||
|
||||
/** 查询条件:重量段、费用项、分区或国家 */
|
||||
private String weightKeyword;
|
||||
|
||||
/** 预览时标记库里是否已有同一文件、同一工作表、同一产品 */
|
||||
private Boolean duplicated;
|
||||
|
||||
/** 已存在记录的主键,覆盖保存时使用 */
|
||||
private Long existsId;
|
||||
|
||||
private List<PayWeightBand> weightBands;
|
||||
|
||||
private List<PaySurcharge> surcharges;
|
||||
|
||||
private List<PayExtraLine> extraLines;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 应付产品某一版价格的有效期。同一产品多次导入,每版一行。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PayProductTerm extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long termId;
|
||||
|
||||
/** 字典 payment_product_type,同一产品的各版本用它归组 */
|
||||
private String dictValue;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private Integer versionNo;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveStart;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveEnd;
|
||||
|
||||
/** 0 生效,1 失效 */
|
||||
private String status;
|
||||
|
||||
private String productName;
|
||||
|
||||
private String sourceFile;
|
||||
|
||||
/** 这一版源文件的 OSS 地址,空表示导入时还没有存文件 */
|
||||
private String filePath;
|
||||
|
||||
private Integer weightCount;
|
||||
|
||||
private Integer surchargeCount;
|
||||
|
||||
private String currency;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 预览确认后的保存请求。只保存前端勾选并回传的产品。
|
||||
*/
|
||||
@Data
|
||||
public class PaySaveCommand
|
||||
{
|
||||
/** 为 true 时,同文件同表同名的旧产品先删后插 */
|
||||
private boolean overwrite;
|
||||
|
||||
private List<PayProduct> products = new ArrayList<>();
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 应付产品附加费。生效开始和结束都空时,表示跟随价卡生效日,不另设窗口。
|
||||
*/
|
||||
@Data
|
||||
public class PaySurcharge
|
||||
{
|
||||
private Long surchargeId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 所属价卡版本,跟产品版本一起变 */
|
||||
private Integer versionNo;
|
||||
|
||||
private String sectionName;
|
||||
|
||||
private String feeName;
|
||||
|
||||
private String zoneName;
|
||||
|
||||
/** 收费标准原文 */
|
||||
private String chargeText;
|
||||
|
||||
/** 从收费标准里解析出的金额;百分比时这里是百分数 */
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 币种代码,计费时和订单币种比对 */
|
||||
private String currency;
|
||||
|
||||
/** 货币和计费单位,与重量段单位同一写法,如 EUR/ITEM、RMB/KG、% */
|
||||
private String priceUnit;
|
||||
|
||||
/** 费用说明 */
|
||||
private String feeDesc;
|
||||
|
||||
/** 时效原文,解析不出日期时仍保留 */
|
||||
private String periodText;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveStart;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveEnd;
|
||||
|
||||
/** 固定收取、产生时收取、旺季期间 */
|
||||
private String chargeTiming;
|
||||
|
||||
/** ALWAYS、POSTCODE、ZONE、WEIGHT、SIZE、RETURN、EXTRA_KG、MANUAL */
|
||||
private String calcRule;
|
||||
|
||||
/** 邮编列表、重量阈值或尺寸表达式。续重时这里是公斤阈值 */
|
||||
private String calcParam;
|
||||
|
||||
/** 偏远邮编规则。续重行的阈值在 calcParam,邮编放这里,避免互相覆盖 */
|
||||
private String postcodeParam;
|
||||
|
||||
/** 适用重量下限,空表示不限。首重从 0 开始 */
|
||||
private BigDecimal weightMin;
|
||||
|
||||
/** 适用重量上限,空表示不限。分区价按这个区间只收一档 */
|
||||
private BigDecimal weightMax;
|
||||
|
||||
private Integer sortNo;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 应付产品上的一行重量段、首重续重或分区单价。
|
||||
*/
|
||||
@Data
|
||||
public class PayWeightBand
|
||||
{
|
||||
private Long bandId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 所属价卡版本,跟产品版本一起变 */
|
||||
private Integer versionNo;
|
||||
|
||||
/** 报价区块,例如派送费、偏远附加费 */
|
||||
private String sectionName;
|
||||
|
||||
private String countryName;
|
||||
|
||||
private String countryCode;
|
||||
|
||||
/** 价格列名称,例如英国大陆、马德里高密度 */
|
||||
private String zoneName;
|
||||
|
||||
/** 重量段原文 */
|
||||
private String weightLabel;
|
||||
|
||||
private BigDecimal weightMin;
|
||||
|
||||
private BigDecimal weightMax;
|
||||
|
||||
/** 体积上限,数字加单位,如 10000cm³。空表示这档不看体积 */
|
||||
private String volumeText;
|
||||
|
||||
/** 处理费、派送费、续重等 */
|
||||
private String chargeName;
|
||||
|
||||
private BigDecimal price;
|
||||
|
||||
/** EUR/ITEM、GBP/KG 等 */
|
||||
private String priceUnit;
|
||||
|
||||
private Integer sortNo;
|
||||
}
|
||||
@@ -23,9 +23,6 @@ public class PrcPayBillFee implements Serializable
|
||||
private String currency;
|
||||
private String roundMode;
|
||||
private String remark;
|
||||
|
||||
/** CALC 计算,LADING 提单分摊,MANUAL 人工 */
|
||||
private String feeSource;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
}
|
||||
|
||||
@@ -15,19 +15,12 @@ public class PrcPriceSet extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** Ordinary package (default). */
|
||||
public static final String CATEGORY_PACKAGE = "PACKAGE";
|
||||
/** Boss exclusive quote. */
|
||||
public static final String CATEGORY_EXCLUSIVE = "EXCLUSIVE";
|
||||
|
||||
private Long priceSetId;
|
||||
private String priceSetCode;
|
||||
private String priceSetName;
|
||||
private String priceSetNote;
|
||||
/** FIXED / DISCOUNT */
|
||||
private String setType;
|
||||
/** PACKAGE=ordinary set / EXCLUSIVE=boss exclusive quote */
|
||||
private String setCategory;
|
||||
private BigDecimal defaultDiscount;
|
||||
private String defaultType;
|
||||
private String calculateSign;
|
||||
|
||||
-4
@@ -40,10 +40,6 @@ public class BatchAddIncomeBillFeeRequest implements Serializable
|
||||
private String beginBizDate;
|
||||
private String endBizDate;
|
||||
|
||||
/** Receive/pickup time range (pickup_date), inclusive. */
|
||||
private String beginPickupDate;
|
||||
private String endPickupDate;
|
||||
|
||||
/** Surcharge config id (prc_surcharge.sc_id). */
|
||||
@NotNull(message = "scId required")
|
||||
private Long scId;
|
||||
|
||||
-3
@@ -25,7 +25,4 @@ public class ChargeWeightTemplateRequest implements Serializable
|
||||
private List<Integer> orderStatuses;
|
||||
private String beginBizDate;
|
||||
private String endBizDate;
|
||||
/** Receive/pickup time range (pickup_date), inclusive. */
|
||||
private String beginPickupDate;
|
||||
private String endPickupDate;
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ public class PrcBillingBillListExportRow implements Serializable
|
||||
@Excel(name = "计费时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date bizDate;
|
||||
@Excel(name = "收货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "提货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date pickupDate;
|
||||
@Excel(name = "销售员", width = 12)
|
||||
|
||||
+2
@@ -43,4 +43,6 @@ public class PrcProfitReportProductRow implements Serializable
|
||||
@Excel(name = "未计算提成订单数", width = 20)
|
||||
private Long commissionMissingOrderCount;
|
||||
*/
|
||||
@Excel(name = "产品价或价格缺失订单数", width = 22)
|
||||
private Long incompleteOrderCount;
|
||||
}
|
||||
|
||||
@@ -24,10 +24,6 @@ public class PrcProfitReportQuery extends BaseEntity
|
||||
private String customerCode;
|
||||
/** 客户代码拆分后的多值查询条件。 */
|
||||
private List<String> customerCodes;
|
||||
/** 按客户当前区域筛选。 */
|
||||
private String customerArea;
|
||||
/** 服务端根据客户区域确定的计算规则。 */
|
||||
private String profitRule;
|
||||
private String customerName;
|
||||
/** 客户名称拆分后的多值查询条件。 */
|
||||
private List<String> customerNames;
|
||||
@@ -39,6 +35,6 @@ public class PrcProfitReportQuery extends BaseEntity
|
||||
private String arBillStatus;
|
||||
/** 是否按产品维度分组值下钻订单明细,允许分组字段为空。 */
|
||||
private Boolean productDrilldown;
|
||||
/** 仅用于拒绝已暂停的提成未计算订单下钻请求。 */
|
||||
/** 订单明细弹窗的筛选类型:ALL、INCOMPLETE;COMMISSION_MISSING 暂停使用。 */
|
||||
private String detailType;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public class PrcProfitReportSummary implements Serializable
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long orderCount;
|
||||
private Long incompleteOrderCount;
|
||||
private String currency;
|
||||
private BigDecimal costAmount;
|
||||
private BigDecimal redlineAmount;
|
||||
|
||||
@@ -31,7 +31,4 @@ public class RecalcBillPriceRequest implements Serializable
|
||||
private List<Integer> billingFailStatuses;
|
||||
private String beginBizDate;
|
||||
private String endBizDate;
|
||||
/** Receive/pickup time range (pickup_date), inclusive. */
|
||||
private String beginPickupDate;
|
||||
private String endPickupDate;
|
||||
}
|
||||
|
||||
@@ -43,11 +43,6 @@ public class TabOrderMonitorQuery implements Serializable
|
||||
* 空则服务端回落默认 3,4,5,6,7,8,11(与历史硬编码一致)。
|
||||
*/
|
||||
private List<Integer> orderStatuses;
|
||||
/**
|
||||
* 是否收货 BL_PICKUP。1 已收货,0 未收货。
|
||||
* 空则服务端回落 1,与历史查询一致。
|
||||
*/
|
||||
private Integer blPickup;
|
||||
/** 分批同步计费每批条数 */
|
||||
private Integer batchSize;
|
||||
/** Excel export field names (Java props; export only). */
|
||||
|
||||
@@ -66,7 +66,7 @@ public enum BillTaskFailCode
|
||||
{
|
||||
return NO_CUSTOMER_PACKAGE.code;
|
||||
}
|
||||
if (containsAny(msg, "无生效套餐版本", "套餐未启用", "noActivePackageVersion", "NO_ACTIVE_PACKAGE_VERSION", "packageNotEnabled"))
|
||||
if (containsAny(msg, "无生效套餐版本", "noActivePackageVersion", "NO_ACTIVE_PACKAGE_VERSION"))
|
||||
{
|
||||
return NO_ACTIVE_PACKAGE_VERSION.code;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user