diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java index 4e980bf6..cc8a5d03 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java @@ -305,6 +305,14 @@ 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). */ diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/ExclusiveQuoteController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/ExclusiveQuoteController.java new file mode 100644 index 00000000..2bf94101 --- /dev/null +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/ExclusiveQuoteController.java @@ -0,0 +1,453 @@ +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 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 payload = new HashMap(); + 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 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 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 payload = new HashMap(); + 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 batch = UploadFileBatch.collect(files, file); + java.util.List bytesList = new java.util.ArrayList(); + java.util.List names = new java.util.ArrayList(); + for (MultipartFile item : batch) + { + bytesList.add(item.getBytes()); + names.add(item.getOriginalFilename()); + } + List 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 payload = new HashMap(); + 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 details) + { + String taskId = priceSetService.submitDetailSurchargeAsync(details); + Map payload = new HashMap(); + 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 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)); + } + +} diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PriceSetController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PriceSetController.java index dab60160..18b69178 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PriceSetController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PriceSetController.java @@ -55,6 +55,8 @@ 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 list = priceSetService.selectPrcPriceSetList(set); return getDataTable(list); @@ -64,6 +66,7 @@ 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)); } @@ -72,6 +75,11 @@ 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); @@ -98,6 +106,8 @@ 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)); } @@ -108,6 +118,7 @@ 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)); } diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/PrcProfitReportController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/PrcProfitReportController.java index 3183d9db..23d94dde 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/PrcProfitReportController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/PrcProfitReportController.java @@ -32,6 +32,7 @@ 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; @@ -58,6 +59,14 @@ public class PrcProfitReportController extends BaseController 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) @@ -118,7 +127,7 @@ public class PrcProfitReportController extends BaseController try { List rows = profitReportService.selectOrderRows(query); - return finishExport(export, rows, PrcProfitReportOrderRow.class, "订单毛利报表"); + return finishExport(export, rows, PrcProfitReportOrderRow.class, "订单毛利报表", query.getProfitRule()); } catch (RuntimeException e) { @@ -136,7 +145,7 @@ public class PrcProfitReportController extends BaseController try { List rows = profitReportService.selectProductRows(query); - return finishExport(export, rows, PrcProfitReportProductRow.class, "产品毛利报表"); + return finishExport(export, rows, PrcProfitReportProductRow.class, "产品毛利报表", query.getProfitRule()); } catch (RuntimeException e) { @@ -200,7 +209,8 @@ public class PrcProfitReportController extends BaseController return export; } - private AjaxResult finishExport(PrcProfitReportExport export, List rows, Class rowType, String sheetName) + private AjaxResult finishExport(PrcProfitReportExport export, List rows, Class rowType, String sheetName, + String profitRule) { Path temp = null; try @@ -208,7 +218,12 @@ public class PrcProfitReportController extends BaseController temp = Files.createTempFile("profit-report-", ".xlsx"); try (OutputStream output = Files.newOutputStream(temp)) { - new ExcelUtil(rowType).exportExcelToStream(rows, sheetName, output); + ExcelUtil excelUtil = new ExcelUtil<>(rowType); + if (ProfitAreaRule.GUANGZHOU.name().equals(profitRule)) + { + excelUtil.hideColumn("costAmount", "redlineGrossProfit", "redlineGrossMarginRate"); + } + excelUtil.exportExcelToStream(rows, sheetName, output); } String objectKey = "report/profit/" + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + ".xlsx"; export.setStorageKey(ossStorage.upload(temp, objectKey, diff --git a/bms-back/bms-admin/src/main/resources/i18n/messages.properties b/bms-back/bms-admin/src/main/resources/i18n/messages.properties index 62b2bcf4..8d1b9ed0 100644 --- a/bms-back/bms-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/bms-admin/src/main/resources/i18n/messages.properties @@ -224,6 +224,11 @@ 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} 条,请缩小筛选范围 @@ -232,3 +237,6 @@ 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不能为空 diff --git a/bms-back/bms-admin/src/test/java/com/bms/report/ProfitAreaRuleTest.java b/bms-back/bms-admin/src/test/java/com/bms/report/ProfitAreaRuleTest.java new file mode 100644 index 00000000..297deb98 --- /dev/null +++ b/bms-back/bms-admin/src/test/java/com/bms/report/ProfitAreaRuleTest.java @@ -0,0 +1,53 @@ +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")); + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java index dc1acbb4..87499e98 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java @@ -1733,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 -> "未命中价格"; diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/domain/BmsCustomer.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/domain/BmsCustomer.java index e78df049..1d67cbb4 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/domain/BmsCustomer.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/domain/BmsCustomer.java @@ -47,9 +47,9 @@ public class BmsCustomer extends BaseEntity @Excel(name = "业务类型", readConverterExp = "0=直客,1=同行,2=集运") private String customerType; - /** 区域类型(字典 data_area_type) */ - @Excel(name = "区域类型", dictType = "data_area_type") - private String areaType; + /** 客户区域(字典 customer_area) */ + @Excel(name = "客户区域", dictType = "customer_area") + private String customerArea; /** 启用状态(0正常 1停用) */ @Excel(name = "启用状态", readConverterExp = "0=正常,1=停用") diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java index 43bd4133..fbc69aae 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java @@ -45,10 +45,10 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService private static final ThreadLocal DATE_FMT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); - /** 区域类型走字典 data_area_type;缓存未命中时保留原编码。 */ - private static String areaTypeLabel(String value) + /** 客户区域走字典 customer_area;缓存未命中时保留原编码。 */ + private static String customerAreaLabel(String value) { - String label = DictUtils.getDictLabel("data_area_type", value); + String label = DictUtils.getDictLabel("customer_area", 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 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("areaType", "区域类型", BmsCustomer::getAreaType, false, BmsCustomerLogServiceImpl::areaTypeLabel), + new FieldDef("customerArea", "客户区域", BmsCustomer::getCustomerArea, false, BmsCustomerLogServiceImpl::customerAreaLabel), 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) && !"areaType".equals(def.field)) + // 合同起止、客户区域允许清空;其余动态字段未提交时不落库,也不记日志 + if (raw == null && !isContractDateField(def.field) && !"customerArea".equals(def.field)) { continue; } diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/sync/CustomerSyncService.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/sync/CustomerSyncService.java index 9b45cb26..ccce88fd 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/sync/CustomerSyncService.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/sync/CustomerSyncService.java @@ -60,6 +60,7 @@ 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; /** 告警明细最多打印条数 */ @@ -178,6 +179,7 @@ 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, diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/BatchAddIncomeBillFeeTask.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/BatchAddIncomeBillFeeTask.java index efe84a48..322c386f 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/BatchAddIncomeBillFeeTask.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/BatchAddIncomeBillFeeTask.java @@ -11,6 +11,7 @@ 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; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPriceSet.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPriceSet.java index 32ec60c5..5bcc1102 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPriceSet.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPriceSet.java @@ -15,12 +15,19 @@ 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; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcProfitReportQuery.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcProfitReportQuery.java index 8a9b4a09..6e62a0bc 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcProfitReportQuery.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcProfitReportQuery.java @@ -24,6 +24,10 @@ public class PrcProfitReportQuery extends BaseEntity private String customerCode; /** 客户代码拆分后的多值查询条件。 */ private List customerCodes; + /** 按客户当前区域筛选。 */ + private String customerArea; + /** 服务端根据客户区域确定的计算规则。 */ + private String profitRule; private String customerName; /** 客户名称拆分后的多值查询条件。 */ private List customerNames; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TabOrderMonitorQuery.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TabOrderMonitorQuery.java index 0d712110..4b66be56 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TabOrderMonitorQuery.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TabOrderMonitorQuery.java @@ -43,6 +43,11 @@ public class TabOrderMonitorQuery implements Serializable * 空则服务端回落默认 3,4,5,6,7,8,11(与历史硬编码一致)。 */ private List orderStatuses; + /** + * 是否收货 BL_PICKUP。1 已收货,0 未收货。 + * 空则服务端回落 1,与历史查询一致。 + */ + private Integer blPickup; /** 分批同步计费每批条数 */ private Integer batchSize; /** Excel export field names (Java props; export only). */ diff --git a/bms-back/bms-system/src/main/java/com/bms/price/enums/BillTaskFailCode.java b/bms-back/bms-system/src/main/java/com/bms/price/enums/BillTaskFailCode.java index bd673244..d3471987 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/enums/BillTaskFailCode.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/enums/BillTaskFailCode.java @@ -66,7 +66,7 @@ public enum BillTaskFailCode { return NO_CUSTOMER_PACKAGE.code; } - if (containsAny(msg, "无生效套餐版本", "noActivePackageVersion", "NO_ACTIVE_PACKAGE_VERSION")) + if (containsAny(msg, "无生效套餐版本", "套餐未启用", "noActivePackageVersion", "NO_ACTIVE_PACKAGE_VERSION", "packageNotEnabled")) { return NO_ACTIVE_PACKAGE_VERSION.code; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/enums/ProfitAreaRule.java b/bms-back/bms-system/src/main/java/com/bms/price/enums/ProfitAreaRule.java new file mode 100644 index 00000000..5e8629b3 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/enums/ProfitAreaRule.java @@ -0,0 +1,43 @@ +package com.bms.price.enums; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** 客户区域字典值对应的毛利计算规则。 */ +public enum ProfitAreaRule +{ + SHENZHEN("C"), + GUANGZHOU("GZ"); + + private final String customerArea; + + ProfitAreaRule(String customerArea) + { + this.customerArea = customerArea; + } + + public static ProfitAreaRule fromCustomerArea(String customerArea) + { + if (customerArea != null) + { + for (ProfitAreaRule rule : values()) + { + if (rule.customerArea.equals(customerArea.trim())) + { + return rule; + } + } + } + return null; + } + + public static Map areaRules() + { + Map result = new LinkedHashMap<>(); + for (ProfitAreaRule rule : values()) + { + result.put(rule.customerArea, rule.name()); + } + return result; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/OracleUpstreamOrderMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/OracleUpstreamOrderMapper.java index c2eb2cbf..c7f23bc5 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/OracleUpstreamOrderMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/OracleUpstreamOrderMapper.java @@ -70,7 +70,8 @@ public interface OracleUpstreamOrderMapper List> selectMatchedTransferOrderKeys( @Param("orderTable") String orderTable, @Param("batch") List batch, - @Param("customerCodes") List customerCodes); + @Param("customerCodes") List customerCodes, + @Param("blPickup") Integer blPickup); /** 按已收集到的订单主键批量查明细(一批为一个内存分页切片)。 */ diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPriceSetDetailMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPriceSetDetailMapper.java index 78e0b458..38b8bfb0 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPriceSetDetailMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPriceSetDetailMapper.java @@ -82,6 +82,18 @@ public interface PrcPriceSetDetailMapper @Param("countryName") String countryName, @Param("zoneCode") String zoneCode); + /** + * Next FIXED weight band whose weight_min is strictly above {@code weight} + * (ceil into the gap between bands, e.g. 1.001 -> 1.01~2). + */ + public PrcPriceSetDetail selectFixedNextBandUp( + @Param("psvId") Long psvId, + @Param("productCode") String productCode, + @Param("countryCode") String countryCode, + @Param("countryName") String countryName, + @Param("zoneCode") String zoneCode, + @Param("weight") BigDecimal weight); + public int countZonedByPsvProductCountry( @Param("psvId") Long psvId, @Param("productCode") String productCode, diff --git a/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderKeyCollectWorker.java b/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderKeyCollectWorker.java index 80346f6c..06cb62d8 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderKeyCollectWorker.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderKeyCollectWorker.java @@ -28,8 +28,9 @@ public class OracleOrderKeyCollectWorker /** 比对回填转单号专用分批(不走三列 UNION)。 */ @DataSource(DataSourceType.SLAVE) - public List> collectTransferBatch(String table, List batch, List customerCodes) + public List> collectTransferBatch(String table, List batch, List customerCodes, + Integer blPickup) { - return oracleUpstreamOrderMapper.selectMatchedTransferOrderKeys(table, batch, customerCodes); + return oracleUpstreamOrderMapper.selectMatchedTransferOrderKeys(table, batch, customerCodes, blPickup); } } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/oracle/TabOrderMonitorService.java b/bms-back/bms-system/src/main/java/com/bms/price/oracle/TabOrderMonitorService.java index 3436f21c..1679feb8 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/oracle/TabOrderMonitorService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/oracle/TabOrderMonitorService.java @@ -456,12 +456,13 @@ public class TabOrderMonitorService } } List customerCodes = cleanedCodes.isEmpty() ? null : cleanedCodes; + normalizeBlPickup(query); int[] page = normalizePage(pageNum, pageSize); int pn = page[0]; int ps = page[1]; String table = resolveTable(); long t0 = System.currentTimeMillis(); - List keys = doCollectTransferKeys(table, nos, customerCodes); + List keys = doCollectTransferKeys(table, nos, customerCodes, query.getBlPickup()); List rows = doPageByKeys(table, keys, pn, ps); log.info("oracle tab_order transfer-fill page transferNos={} matched={} page={}/{} rows={} costMs={}", Integer.valueOf(nos.size()), Integer.valueOf(keys.size()), @@ -795,12 +796,14 @@ public class TabOrderMonitorService * 回填转单号专用收集:只走 selectMatchedTransferOrderKeys, * 不走三列 UNION、不带提货时间。 */ - private List doCollectTransferKeys(String table, List transferNos, List customerCodes) + private List doCollectTransferKeys(String table, List transferNos, List customerCodes, + Integer blPickup) { StringBuilder fp = new StringBuilder("tf|").append(table == null ? "" : table).append('|'); appendSorted(fp, transferNos); fp.append('|'); appendSorted(fp, customerCodes); + fp.append('|').append(blPickup == null ? "" : blPickup); String fingerprint = Integer.toHexString(fp.toString().hashCode()); long now = System.currentTimeMillis(); CacheEntry cached = collectCache.get(fingerprint); @@ -821,7 +824,7 @@ public class TabOrderMonitorService new ArrayList>>>(batches.size()); for (List batch : batches) { - futures.add(collectPool.submit(() -> collectWorker.collectTransferBatch(table, batch, customerCodes))); + futures.add(collectPool.submit(() -> collectWorker.collectTransferBatch(table, batch, customerCodes, blPickup))); } Map unique = new LinkedHashMap(); @@ -1035,6 +1038,7 @@ public class TabOrderMonitorService .append('|').append(safe(q.getOrderId())).append('|').append(safe(q.getOrgOrderId())) .append('|').append(safe(q.getTransferOrderId())).append('|'); appendSortedInts(sb, q.getOrderStatuses()); + sb.append('|').append(q.getBlPickup() == null ? "" : q.getBlPickup()); return Integer.toHexString(sb.toString().hashCode()); } @@ -1207,6 +1211,7 @@ public class TabOrderMonitorService query.setOrgOrderId(trimToNull(query.getOrgOrderId())); query.setTransferOrderId(trimToNull(query.getTransferOrderId())); normalizeOrderStatuses(query); + normalizeBlPickup(query); } /** Empty/missing -> default 3,4,5,6,7,8,11 so SQL always has a status IN list. */ @@ -1227,6 +1232,16 @@ public class TabOrderMonitorService query.setOrderStatuses(cleaned.isEmpty() ? new ArrayList(DEFAULT_ORDER_STATUSES) : cleaned); } + /** Empty or anything other than 0/1 falls back to 1(已收货). */ + private static void normalizeBlPickup(TabOrderMonitorQuery query) + { + Integer blPickup = query.getBlPickup(); + if (blPickup == null || (blPickup.intValue() != 0 && blPickup.intValue() != 1)) + { + query.setBlPickup(Integer.valueOf(1)); + } + } + /** * 多行文本归一化:按换行/逗号/分号/顿号拆分(不按空格拆),trim、去空去重。 */ @@ -1309,8 +1324,9 @@ public class TabOrderMonitorService vo.setTrackId(str(row, "TRACK_ID")); vo.setCustomerCode(str(row, "CUSTOMER_CODE")); vo.setCustomerName(str(row, "CUSTOMER_NAME")); - vo.setProductCode(str(row, "PRODUCT_CODE")); - vo.setProductName(str(row, "PRODUCT_NAME")); + // Prefer LINE_CODE / LINE_NAME (billing product); list SQL aliases them as PRODUCT_*. + vo.setProductCode(firstStr(row, "LINE_CODE", "PRODUCT_CODE")); + vo.setProductName(firstStr(row, "LINE_NAME", "PRODUCT_NAME")); vo.setProductFloorCode(str(row, "PRODUCT_FLOOR_CODE")); vo.setCountry(str(row, "COUNTRY")); vo.setCountryName(str(row, "COUNTRY_NAME")); @@ -1330,6 +1346,23 @@ public class TabOrderMonitorService return v == null ? null : String.valueOf(v).trim(); } + private static String firstStr(Map row, String... keys) + { + if (keys == null) + { + return null; + } + for (String key : keys) + { + String v = str(row, key); + if (StringUtils.isNotEmpty(v)) + { + return v; + } + } + return null; + } + private static Integer integer(Map row, String key) { Object v = value(row, key); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IBmsProductService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IBmsProductService.java index 53c547b7..1797e4c2 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IBmsProductService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IBmsProductService.java @@ -59,10 +59,6 @@ public interface IBmsProductService public java.util.Map> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception; - /** - * Parse single-country dedicated-line quote Excel (same format as product country import). - * All weight rows are stamped with the selected country. Keys are upper-case product codes. - */ public java.util.Map> parseCountryQuoteExcelByProduct(byte[] bytes, String filename, String countryCode) throws Exception; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java index a0747cf0..b5db8a2a 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java @@ -166,6 +166,9 @@ public interface IPrcOrderBillService /** Poll Redis progress for local rebill task. */ BatchAddIncomeBillFeeTask getLocalRebillTask(String taskId); + /** Cooperative cancel for a running local rebill task. */ + BatchAddIncomeBillFeeTask cancelLocalRebill(String taskId); + /** Data-scoped bill_id list for local rebill (same filters as order-bill list). */ List selectBillIdsForLocalRebill(PrcOrderBill query); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcPriceSetService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcPriceSetService.java index e2d9dd2f..6690f220 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcPriceSetService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcPriceSetService.java @@ -22,6 +22,9 @@ public interface IPrcPriceSetService public List selectPrcPriceSetList(PrcPriceSet set); + /** Fail if the package is not in the expected category (PACKAGE / EXCLUSIVE). */ + public void assertSetCategory(Long priceSetId, String expectedCategory); + public Long savePrcPriceSet(PrcPriceSet set); public int updatePrcPriceSet(PrcPriceSet set); @@ -149,6 +152,13 @@ public interface IPrcPriceSetService boolean updateOnly, List existingDetails, boolean replaceByProductCountry, String countryCode) throws Exception; + /** + * @param exclusiveTemplate true = HALARA large-customer exclusive quote Excel (destination / handling fee) + */ + public Map previewImportDetailPricesMany(List bytesList, List names, + boolean updateOnly, List existingDetails, boolean replaceByProductCountry, String countryCode, + boolean exclusiveTemplate) throws Exception; + /** * Sync worker: parse bytes and upsert FIXED detail prices matched by * product + country + zone(if any) + weight band (insert when missing unless updateOnly). diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcProfitReportService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcProfitReportService.java index ad214559..ec23604b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcProfitReportService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcProfitReportService.java @@ -1,6 +1,7 @@ package com.bms.price.service; import java.util.List; +import java.util.Map; import com.bms.customer.customerinfo.domain.BmsCustomerOption; // 提成功能暂停使用:import com.bms.price.domain.PrcProfitCommissionRate; import com.bms.price.domain.PrcProfitReportExport; @@ -14,6 +15,8 @@ public interface IPrcProfitReportService { List selectCustomerOptions(String keywords); + Map selectAreaRules(); + List selectOrderRows(PrcProfitReportQuery query); List selectProductRows(PrcProfitReportQuery query); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java index b44c7018..36979d64 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java @@ -2574,7 +2574,7 @@ public class BmsProductServiceImpl implements IBmsProductService Date sheetEffective = parseSheetEffectiveDate(sheet); List list = new ArrayList<>(); - int[] countryHeader = findHeaderCell(sheet, "国家"); + int[] countryHeader = findHeaderCell(sheet, "\u56fd\u5bb6"); if (countryHeader == null) { return list; @@ -2582,12 +2582,12 @@ public class BmsProductServiceImpl implements IBmsProductService int headerRowIdx = countryHeader[0]; Row headerRow = sheet.getRow(headerRowIdx); int colCountry = countryHeader[1]; - int colWeight = findColByHeader(headerRow, "重量", colCountry + 1); - int colFreight = findColByHeader(headerRow, "运费", colCountry + 1); - int colRegister = findColByHeader(headerRow, "挂号", colCountry + 1); - int colSize = findColByHeader(headerRow, "尺寸", colCountry + 1); - int colLastMile = findColByHeader(headerRow, "尾程", colCountry + 1); - int colLeadTime = findColByHeader(headerRow, "时效", colCountry + 1); + int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", colCountry + 1); + int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", colCountry + 1); + int colRegister = findColByHeader(headerRow, "\u6302\u53f7", colCountry + 1); + int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", colCountry + 1); + int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", colCountry + 1); + int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", colCountry + 1); if (colWeight < 0) { colWeight = colCountry + 1; @@ -2627,7 +2627,8 @@ public class BmsProductServiceImpl implements IBmsProductService } String countryCell = cellString(row.getCell(colCountry)); String weightLabel = cellString(row.getCell(colWeight)); - if ("国家".equals(countryCell) || weightLabel.contains("重量段")) + if ("\u56fd\u5bb6".equals(countryCell) + || (weightLabel != null && weightLabel.contains("\u91cd\u91cf\u6bb5"))) { continue; } @@ -2847,7 +2848,8 @@ public class BmsProductServiceImpl implements IBmsProductService { return null; } - Matcher m = WEIGHT_RANGE_PATTERN.matcher(label.replace(" ", "")); + String compact = label.replace(" ", "").replace("\u3000", ""); + Matcher m = WEIGHT_RANGE_PATTERN.matcher(compact); if (!m.find()) { return null; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java index 2b65f2ab..5b38b12f 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java @@ -972,6 +972,25 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService return localRebillAsyncRunner.get(taskId); } + @Override + public BatchAddIncomeBillFeeTask cancelLocalRebill(String taskId) + { + if (StringUtils.isEmpty(taskId)) + { + throw new ServiceException(MessageUtils.message("price.bill.recalc.taskIdRequired")); + } + if (localRebillAsyncRunner == null) + { + throw new ServiceException(MessageUtils.message("price.bill.recalc.taskNotExists")); + } + BatchAddIncomeBillFeeTask task = localRebillAsyncRunner.requestCancel(taskId.trim()); + if (task == null) + { + throw new ServiceException(MessageUtils.message("price.bill.recalc.taskNotExists")); + } + return task; + } + @Override public String submitOrderBillExport(String exportType, PrcOrderBill query) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java index 1c060e66..041cf63a 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java @@ -47,6 +47,7 @@ import com.bms.price.mapper.PrcPriceSetVersionMapper; import com.bms.price.mapper.PrcSurchargeMapper; import com.bms.price.mapper.PrcSurchargeRelMapper; import com.bms.price.service.IBmsProductService; +import com.bms.price.service.support.ExclusiveHalaraQuoteExcelParser; import com.bms.price.search.IBillSearchStore; import com.bms.price.search.PriceChangeEsLog; import com.bms.price.service.IPrcPriceExcelFileService; @@ -68,6 +69,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService private static final String STATUS_REJECT = "R"; private static final String STATUS_STOP = "S"; private static final String TYPE_FIXED = "FIXED"; + private static final String CATEGORY_PACKAGE = PrcPriceSet.CATEGORY_PACKAGE; + private static final String CATEGORY_EXCLUSIVE = PrcPriceSet.CATEGORY_EXCLUSIVE; private static final String TYPE_DISCOUNT = "DISCOUNT"; private static final String VER_DRAFT = "0"; private static final String VER_EFFECT = "1"; @@ -114,6 +117,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService @Autowired private IBmsProductService productService; + @Autowired + private ExclusiveHalaraQuoteExcelParser exclusiveHalaraQuoteExcelParser; + @Lazy @Autowired private PrcPriceSetSaveAsyncRunner saveAsyncRunner; @@ -228,10 +234,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { set.setSetType(TYPE_FIXED); } + set.setSetCategory(normalizeSetCategory(set.getSetCategory())); setMapper.insertPrcPriceSet(set); if (StringUtils.isEmpty(set.getPriceSetCode())) { - set.setPriceSetCode(String.format("SET%04d", set.getPriceSetId())); + set.setPriceSetCode(buildPriceSetCode(set.getSetCategory(), set.getPriceSetId())); setMapper.updatePrcPriceSet(set); } } @@ -240,6 +247,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet db = requireSet(set.getPriceSetId()); assertBasicEditable(db); set.setStatus(db.getStatus()); + // Category is immutable after create; keep DB value for name uniqueness scope. + set.setSetCategory(normalizeSetCategory(db.getSetCategory())); setMapper.updatePrcPriceSet(set); } reportSaveProgress(set, 20, "saving version"); @@ -275,6 +284,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService throw new ServiceException(MessageUtils.message("price.set.name.empty")); } set.setPriceSetName(set.getPriceSetName().trim()); + set.setSetCategory(normalizeSetCategory(db.getSetCategory())); if (setMapper.countByName(set) > 0) { throw new ServiceException(MessageUtils.message("price.set.name.exists")); @@ -641,6 +651,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService copy.setPriceSetName(src.getPriceSetName() + "-副本"); copy.setPriceSetNote(src.getPriceSetNote()); copy.setSetType(src.getSetType()); + copy.setSetCategory(normalizeSetCategory(src.getSetCategory())); copy.setDefaultDiscount(src.getDefaultDiscount()); copy.setDefaultType(src.getDefaultType()); copy.setCalculateSign(src.getCalculateSign()); @@ -731,8 +742,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { set.setCustomers(db.getCustomers()); } + if (StringUtils.isEmpty(set.getSetCategory())) + { + set.setSetCategory(db.getSetCategory()); + } } } + set.setSetCategory(normalizeSetCategory(set.getSetCategory())); int nameCount = setMapper.countByName(set); if (StringUtils.isEmpty(set.getPriceSetName())) @@ -1201,9 +1217,18 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService @Override public Map previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly, List existingDetails, boolean replaceByProductCountry, String countryCode) throws Exception + { + return previewImportDetailPrices(bytes, filename, updateOnly, existingDetails, replaceByProductCountry, + countryCode, false); + } + + public Map previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly, + List existingDetails, boolean replaceByProductCountry, String countryCode, + boolean exclusiveTemplate) throws Exception { boolean replaceScope = replaceByProductCountry || StringUtils.isNotEmpty(countryCode); - Map> byProduct = parseSetImportExcel(bytes, filename, countryCode); + Map> byProduct = parseSetImportExcel(bytes, filename, countryCode, + exclusiveTemplate); if (byProduct == null || byProduct.isEmpty()) { throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows")); @@ -1391,6 +1416,14 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService public Map previewImportDetailPricesMany(List bytesList, List names, boolean updateOnly, List existingDetails, boolean replaceByProductCountry, String countryCode) throws Exception + { + return previewImportDetailPricesMany(bytesList, names, updateOnly, existingDetails, replaceByProductCountry, + countryCode, false); + } + + public Map previewImportDetailPricesMany(List bytesList, List names, + boolean updateOnly, List existingDetails, boolean replaceByProductCountry, String countryCode, + boolean exclusiveTemplate) throws Exception { if (bytesList == null || bytesList.isEmpty()) { @@ -1407,7 +1440,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { String filename = names != null && i < names.size() ? names.get(i) : null; last = previewImportDetailPrices(bytesList.get(i), filename, updateOnly, existing, replaceByProductCountry, - countryCode); + countryCode, exclusiveTemplate); Object details = last == null ? null : last.get("details"); if (details instanceof List) { @@ -1429,6 +1462,21 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService private Map> parseSetImportExcel(byte[] bytes, String filename, String countryCode) throws Exception { + return parseSetImportExcel(bytes, filename, countryCode, false); + } + + private Map> parseSetImportExcel(byte[] bytes, String filename, String countryCode, + boolean exclusiveTemplate) throws Exception + { + if (exclusiveTemplate) + { + if (StringUtils.isNotEmpty(countryCode)) + { + throw new ServiceException( + "\u4e13\u5c5e\u62a5\u4ef7\u4e0d\u652f\u6301\u5355\u56fd\u5bfc\u5165\uff0c\u8bf7\u4e0a\u4f20\u5b8c\u6574\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868"); + } + return exclusiveHalaraQuoteExcelParser.parse(bytes, filename); + } if (StringUtils.isNotEmpty(countryCode)) { return productService.parseCountryQuoteExcelByProduct(bytes, filename, countryCode); @@ -1551,7 +1599,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } reportImportProgress(taskId, priceSetId, 20, "parsing excel"); boolean replaceScope = replaceByProductCountry || StringUtils.isNotEmpty(countryCode); - Map> byProduct = parseSetImportExcel(bytes, filename, countryCode); + boolean exclusiveTemplate = CATEGORY_EXCLUSIVE.equals(normalizeSetCategory(db.getSetCategory())); + Map> byProduct = parseSetImportExcel(bytes, filename, countryCode, + exclusiveTemplate); if (byProduct == null || byProduct.isEmpty()) { throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows")); @@ -2746,6 +2796,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService throw new ServiceException(MessageUtils.message("price.set.discount.range")); } } + if (StringUtils.isEmpty(set.getSetCategory()) && set.getPriceSetId() != null) + { + PrcPriceSet db = setMapper.selectPrcPriceSetHeaderById(set.getPriceSetId()); + if (db != null) + { + set.setSetCategory(db.getSetCategory()); + } + } + set.setSetCategory(normalizeSetCategory(set.getSetCategory())); if (setMapper.countByName(set) > 0) { throw new ServiceException(MessageUtils.message("price.set.name.exists")); @@ -2756,6 +2815,34 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } } + /** PACKAGE (default) or EXCLUSIVE. */ + public static String normalizeSetCategory(String setCategory) + { + if (CATEGORY_EXCLUSIVE.equalsIgnoreCase(StringUtils.trim(setCategory))) + { + return CATEGORY_EXCLUSIVE; + } + return CATEGORY_PACKAGE; + } + + private static String buildPriceSetCode(String setCategory, Long priceSetId) + { + String prefix = CATEGORY_EXCLUSIVE.equals(normalizeSetCategory(setCategory)) ? "EXC" : "SET"; + return String.format("%s%04d", prefix, priceSetId); + } + + /** Ensure the package belongs to the expected module category. */ + public void assertSetCategory(Long priceSetId, String expectedCategory) + { + PrcPriceSet db = requireSet(priceSetId); + String actual = normalizeSetCategory(db.getSetCategory()); + String expect = normalizeSetCategory(expectedCategory); + if (!expect.equals(actual)) + { + throw new ServiceException(MessageUtils.message("price.set.notExists")); + } + } + private void validateDetail(PrcPriceSet set, PrcPriceSetDetail detail) { if (StringUtils.isEmpty(detail.getProductCode())) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcProfitReportServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcProfitReportServiceImpl.java index 8ab3023c..594f6d6d 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcProfitReportServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcProfitReportServiceImpl.java @@ -3,6 +3,7 @@ package com.bms.price.service.impl; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; // 提成功能暂停使用:import java.math.BigDecimal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -19,6 +20,7 @@ import com.bms.price.domain.vo.PrcProfitReportOrderRow; import com.bms.price.domain.vo.PrcProfitReportProductRow; import com.bms.price.domain.vo.PrcProfitReportQuery; import com.bms.price.domain.vo.PrcProfitReportSummary; +import com.bms.price.enums.ProfitAreaRule; import com.bms.price.mapper.PrcProfitReportMapper; import com.bms.price.service.IPrcProfitReportService; @@ -48,6 +50,12 @@ public class PrcProfitReportServiceImpl implements IPrcProfitReportService return customerMapper.selectCustomerOptions(query); } + @Override + public Map selectAreaRules() + { + return ProfitAreaRule.areaRules(); + } + @Override public List selectOrderRows(PrcProfitReportQuery query) { @@ -105,12 +113,23 @@ public class PrcProfitReportServiceImpl implements IPrcProfitReportService { if (query == null) { - return; + throw new ServiceException("请选择客户区域"); } if ("COMMISSION_MISSING".equals(query.getDetailType())) { throw new ServiceException("提成功能已暂停使用"); } + if (query.getCustomerArea() == null || query.getCustomerArea().trim().isEmpty()) + { + throw new ServiceException("请选择客户区域"); + } + ProfitAreaRule rule = ProfitAreaRule.fromCustomerArea(query.getCustomerArea()); + if (rule == null) + { + throw new ServiceException("该客户区域未配置毛利规则"); + } + query.setCustomerArea(query.getCustomerArea().trim()); + query.setProfitRule(rule.name()); query.setOrderNos(splitFilterValues(query.getOrderNo(), "订单号")); query.setCustomerCodes(splitFilterValues(query.getCustomerCode(), "客户代码")); query.setCustomerNames(splitFilterValues(query.getCustomerName(), "客户名称")); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java index 08afe8e7..5f996c1b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -410,12 +410,16 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } if (binds == null || binds.isEmpty()) { - String msgKey = request instanceof TmsPriceBillRequest - ? "price.bill.noCustomerPackage" - : "price.quote.noCustomerPackage"; - throw new ServiceException(MessageUtils.message(msgKey)); + if (request instanceof TmsPriceBillRequest) + { + // Soft-fail: persist billing_fail_status=4 on the order bill. + return buildBillingMissResult(request, productCode, productName, countryCode, countryName, + weight, bizDate, BillBillingFailStatus.NO_CUSTOMER_PACKAGE.getCode()); + } + throw new ServiceException(MessageUtils.message("price.quote.noCustomerPackage")); } + boolean anyEnabledSet = false; boolean anyActiveVersion = false; boolean productExcludedBySetProducts = false; boolean attemptedPriceMatch = false; @@ -428,10 +432,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService for (PrcCustomerPriceSet bind : binds) { PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId()); + // Only enabled (E) sets participate; draft/stop are skipped and recorded as fail below. if (set == null || !"E".equals(set.getStatus())) { continue; } + anyEnabledSet = true; // Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate) PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf); if (version == null || version.getPsvId() == null) @@ -560,9 +566,22 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService if (!anyActiveVersion) { - String msgKey = request instanceof TmsPriceBillRequest - ? "price.bill.noActivePackageVersion" - : "price.quote.noActivePackageVersion"; + if (request instanceof TmsPriceBillRequest) + { + // Soft-fail: package bound but not enabled, or enabled with no version at bizDate. + // Persists billing_fail_status=5 so local rebill / MQ bill both record the miss. + TmsPriceQuoteResult miss = buildBillingMissResult(request, productCode, productName, + countryCode, countryName, weight, bizDate, + BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode()); + if (!anyEnabledSet) + { + miss.setMatchMessage(MessageUtils.message("price.bill.billingFail.packageNotEnabled")); + } + return miss; + } + String msgKey = anyEnabledSet + ? "price.quote.noActivePackageVersion" + : "price.quote.packageNotEnabled"; throw new ServiceException(MessageUtils.message(msgKey)); } if (!attemptedPriceMatch && productExcludedBySetProducts) @@ -720,6 +739,14 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService { return MessageUtils.message("price.bill.billingFail.catalogWeight"); } + if (missCode == BillBillingFailStatus.NO_CUSTOMER_PACKAGE.getCode()) + { + return MessageUtils.message("price.bill.billingFail.noCustomerPackage"); + } + if (missCode == BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode()) + { + return MessageUtils.message("price.bill.billingFail.noActivePackageVersion"); + } return MessageUtils.message("price.bill.packagePriceNotHit"); } @@ -994,7 +1021,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService countryCode, countryName, zoneCode, weight); BigDecimal billWeight = weight; boolean snappedToMinBand = false; - // Miss: if charge weight is below the lowest band, bill at that band's weight_min. + boolean snappedUpToNextBand = false; + // Miss: below lowest band -> bill at that band's weight_min. if (detail == null) { PrcPriceSetDetail minBand = detailMapper.selectFixedMinBand( @@ -1007,6 +1035,19 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService snappedToMinBand = true; } } + // Miss in a gap (e.g. 1.001 between 0-1 and 1.01-2): ceil to next band's weight_min. + if (detail == null) + { + PrcPriceSetDetail nextBand = detailMapper.selectFixedNextBandUp( + psvId, productCode, countryCode, countryName, zoneCode, weight); + if (nextBand != null && nextBand.getWeightMin() != null) + { + detail = nextBand; + billWeight = nextBand.getWeightMin().setScale(3, RoundingMode.HALF_UP); + snappedUpToNextBand = true; + } + } + // Still no band (above last max, or no details): caller records WEIGHT miss. if (detail == null) { return null; @@ -1016,7 +1057,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return null; } BmsProductCountry useCatalog = catalog; - if (snappedToMinBand) + if (snappedToMinBand || snappedUpToNextBand) { BmsProductCountry rematched = softMatchCatalog( productCode, countryCode, countryName, zoneCode, billWeight, bizDate, resolvedZone); @@ -1039,6 +1080,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService r.setChargeWeight(weight); msg = msg + "; billed at min band " + billWeight.toPlainString() + " (persist original weight)"; } + else if (snappedUpToNextBand) + { + // Gap ceil: bill at next band min (1.001 -> 1.01); persist snapped charge_weight. + r.setWeight(weight); + r.setChargeWeight(billWeight); + msg = msg + "; snapped up " + weight.toPlainString() + " -> " + billWeight.toPlainString(); + } r.setMatchMessage(msg); r.setCustomerId(bind.getCustomerId()); return r; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/support/ExclusiveHalaraQuoteExcelParser.java b/bms-back/bms-system/src/main/java/com/bms/price/service/support/ExclusiveHalaraQuoteExcelParser.java new file mode 100644 index 00000000..bf52f19b --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/support/ExclusiveHalaraQuoteExcelParser.java @@ -0,0 +1,913 @@ +package com.bms.price.service.support; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +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.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.bms.common.exception.ServiceException; +import com.bms.common.utils.StringUtils; +import com.bms.price.domain.BmsProduct; +import com.bms.price.domain.BmsProductCountry; +import com.bms.price.domain.PrcZone; +import com.bms.price.mapper.PrcZoneMapper; +import com.bms.price.util.ZoneCodeAbbrevs; +import com.bms.system.domain.BmsRegion; +import com.bms.system.mapper.BmsRegionMapper; + +/** + * Exclusive / large-customer (HALARA) quote Excel parser. + * Completely separate from ordinary package / product quote Excel parsing. + * + * Catalog columns: category / country / level / product name / price sheet / product code / feature / lead time. + * Price sheets: destination / origin / weight / freight(CNY/KG) / handling(CNY/PCS). + */ +@Component +public class ExclusiveHalaraQuoteExcelParser +{ + private static final String CATALOG_SHEET = "\u4ea7\u54c1\u76ee\u5f55"; + + /** Parse 0.01<=W<=0.5 / 0.3> parse(byte[] bytes, String filename) throws Exception + { + if (bytes == null || bytes.length == 0) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u5bfc\u5165\u6587\u4ef6\u4e3a\u7a7a"); + } + if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx") + || filename.toLowerCase(Locale.ROOT).endsWith(".xls"))) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301 .xls / .xlsx"); + } + try (InputStream is = new ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is)) + { + assertHalaraFormat(workbook); + Sheet catalog = workbook.getSheet(CATALOG_SHEET); + if (catalog == null) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u7f3a\u5c11\u300c\u4ea7\u54c1\u76ee\u5f55\u300d"); + } + List products = dedupeByCode(parseCatalog(catalog)); + if (products.isEmpty()) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ea7\u54c1\u76ee\u5f55\u65e0\u6709\u6548\u4ea7\u54c1"); + } + + Map> ownersBySheet = new LinkedHashMap>(); + for (BmsProduct product : products) + { + if (product == null || StringUtils.isEmpty(product.getProductCode())) + { + continue; + } + Sheet productSheet = findSheetForProduct(workbook, product); + if (productSheet == null) + { + continue; + } + List owners = ownersBySheet.get(productSheet.getSheetName()); + if (owners == null) + { + owners = new ArrayList(); + ownersBySheet.put(productSheet.getSheetName(), owners); + } + owners.add(product); + } + + Map regionByNameCn = loadRegionByNameCn(); + List euRegions = loadEuRegions(regionByNameCn); + Map zoneByCountryAndCode = loadZoneByCountryAndCode(); + Map> out = new LinkedHashMap>(); + List dupTips = new ArrayList(); + + for (int i = 0; i < workbook.getNumberOfSheets(); i++) + { + String sheetName = workbook.getSheetName(i); + if (shouldSkipSheet(sheetName)) + { + continue; + } + Sheet sheet = workbook.getSheetAt(i); + if (sheet == null || findHeaderCell(sheet, "\u76ee\u7684\u5730") == null) + { + continue; + } + List owners = ownersBySheet.get(sheetName); + if (owners == null || owners.isEmpty()) + { + continue; + } + for (BmsProduct owner : owners) + { + List rows = parsePriceSheet(sheet, owner, regionByNameCn, euRegions, + zoneByCountryAndCode); + String tip = dupCountryWeightTip(owner, rows); + if (StringUtils.isNotEmpty(tip)) + { + dupTips.add(tip); + } + if (rows == null || rows.isEmpty()) + { + continue; + } + String key = owner.getProductCode().trim().toUpperCase(Locale.ROOT); + List existing = out.get(key); + if (existing == null) + { + out.put(key, new ArrayList(rows)); + } + else + { + existing.addAll(rows); + } + } + } + if (!dupTips.isEmpty()) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u5b58\u5728\u91cd\u590d\u56fd\u5bb6+\u91cd\u91cf\u6bb5\uff1a
" + String.join("
", dupTips)); + } + if (out.isEmpty()) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u672a\u89e3\u6790\u5230\u6709\u6548\u4ef7\u683c\u884c"); + } + return out; + } + } + + private void assertHalaraFormat(Workbook workbook) + { + boolean hasDest = false; + for (int i = 0; i < workbook.getNumberOfSheets(); i++) + { + String sheetName = workbook.getSheetName(i); + if (shouldSkipSheet(sheetName)) + { + continue; + } + Sheet sheet = workbook.getSheetAt(i); + if (sheet != null && findHeaderCell(sheet, "\u76ee\u7684\u5730") != null) + { + hasDest = true; + break; + } + } + if (!hasDest) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868\uff08\u542b\u300c\u76ee\u7684\u5730\u300d\u300c\u8fd0\u8d39\u300d\u300c\u64cd\u4f5c\u8d39\u300d\u5217\uff09\uff0c\u8bf7\u52ff\u4f7f\u7528\u666e\u901a\u5957\u9910 Excel"); + } + } + + private static boolean shouldSkipSheet(String name) + { + if (StringUtils.isEmpty(name)) + { + return true; + } + String n = name.trim(); + if (CATALOG_SHEET.equals(n)) + { + return true; + } + if ("Sheet1".equalsIgnoreCase(n) || "Sheet2".equalsIgnoreCase(n)) + { + return true; + } + if (n.contains("\u90ae\u7f16") || n.contains("\u8d54\u507f") || n.contains("\u9000\u4ef6") + || n.contains("\u7533\u62a5")) + { + return true; + } + return n.contains("\u7a0e\u7387") || n.toUpperCase(Locale.ROOT).contains("VAT"); + } + + private List parseCatalog(Sheet sheet) + { + List list = new ArrayList(); + int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u4ee3\u7801"); + if (header == null) + { + header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0"); + } + if (header == null) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u672a\u627e\u5230\u8868\u5934\u300c\u4ea7\u54c1\u4ee3\u7801\u300d/\u300c\u4ea7\u54c1\u540d\u79f0\u300d"); + } + int headerRowIdx = header[0]; + Row head = sheet.getRow(headerRowIdx); + int colCode = findColByHeader(head, "\u4ea7\u54c1\u4ee3\u7801", 0); + int colName = findColByHeader(head, "\u4ea7\u54c1\u540d\u79f0", 0); + int colLevel = findColByHeader(head, "\u7b49\u7ea7", 0); + if (colLevel < 0) + { + colLevel = findColByHeader(head, "\u65f6\u6548", 0); + } + int colCargo = findColByHeader(head, "\u4ea7\u54c1\u7279\u6027", 0); + if (colCargo < 0) + { + colCargo = findColByHeader(head, "\u8d70\u8d27\u5c5e\u6027", 0); + } + int colDest = findColByHeader(head, "\u56fd\u5bb6", 0); + if (colDest < 0) + { + colDest = findColByHeader(head, "\u76ee\u7684", 0); + } + int colLead = findColByHeader(head, "\u53c2\u8003\u65f6\u6548", 0); + if (colCode < 0 && colName < 0) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u7f3a\u5c11\u300c\u4ea7\u54c1\u4ee3\u7801\u300d/\u300c\u4ea7\u54c1\u540d\u79f0\u300d\u5217"); + } + int lastRow = sheet.getLastRowNum(); + for (int i = headerRowIdx + 1; i <= lastRow; i++) + { + Row row = sheet.getRow(i); + if (row == null) + { + continue; + } + String rawCode = colCode >= 0 ? cellString(row.getCell(colCode)) : ""; + String name = colName >= 0 ? cellString(row.getCell(colName)) : ""; + if (StringUtils.isEmpty(rawCode) && StringUtils.isEmpty(name)) + { + continue; + } + if ("\u4ea7\u54c1\u4ee3\u7801".equals(rawCode) || "\u4ea7\u54c1\u540d\u79f0".equals(name)) + { + continue; + } + String code = normalizeCode(rawCode); + if (StringUtils.isEmpty(code)) + { + continue; + } + BmsProduct p = new BmsProduct(); + p.setProductCode(code); + p.setProductName(StringUtils.isNotEmpty(name) ? name : code); + if (colLevel >= 0) + { + p.setServiceLevel(cellString(row.getCell(colLevel))); + } + if (colCargo >= 0) + { + p.setCargoAttr(cellString(row.getCell(colCargo))); + } + if (colDest >= 0) + { + p.setDestRegion(cellString(row.getCell(colDest))); + } + if (colLead >= 0) + { + p.setUpdateNote(cellString(row.getCell(colLead))); + } + list.add(p); + } + return list; + } + + /** Keep dots in codes such as GB.PEXP / DE.PEXP. */ + private static String normalizeCode(String raw) + { + if (StringUtils.isEmpty(raw)) + { + return null; + } + String s = raw.trim().replaceAll("[\\(\\uFF08][^\\)\\uFF09]*[\\)\\uFF09]", "").trim(); + if (StringUtils.isEmpty(s)) + { + return null; + } + Matcher m = Pattern.compile("(?i)^([A-Z0-9._-]+)").matcher(s); + if (!m.find()) + { + return null; + } + return m.group(1).toUpperCase(Locale.ROOT); + } + + private List dedupeByCode(List products) + { + Map map = new LinkedHashMap(); + for (BmsProduct p : products) + { + if (p == null || StringUtils.isEmpty(p.getProductCode())) + { + continue; + } + String key = p.getProductCode().trim().toUpperCase(Locale.ROOT); + if (!map.containsKey(key)) + { + map.put(key, p); + } + } + return new ArrayList(map.values()); + } + + private Sheet findSheetForProduct(Workbook workbook, BmsProduct product) + { + if (workbook == null || product == null) + { + return null; + } + String wantName = product.getProductName(); + String wantCode = product.getProductCode(); + if (StringUtils.isNotEmpty(wantName)) + { + Sheet exact = workbook.getSheet(wantName.trim()); + if (exact != null && !shouldSkipSheet(exact.getSheetName())) + { + return exact; + } + } + String nameKey = normalizeKey(wantName); + String codeKey = normalizeKey(wantCode); + String nameBase = sheetNameBase(nameKey); + Sheet best = null; + int bestScore = 0; + for (int i = 0; i < workbook.getNumberOfSheets(); i++) + { + String sheetName = workbook.getSheetName(i); + if (shouldSkipSheet(sheetName)) + { + continue; + } + String key = normalizeKey(sheetName); + if (StringUtils.isEmpty(key)) + { + continue; + } + int score = 0; + if (StringUtils.isNotEmpty(nameKey) && key.equals(nameKey)) + { + return workbook.getSheetAt(i); + } + if (StringUtils.isNotEmpty(nameKey) && key.contains(nameKey)) + { + score = 100; + } + else if (StringUtils.isNotEmpty(nameKey) && nameKey.contains(key) && key.length() >= 4) + { + score = 80; + } + else if (StringUtils.isNotEmpty(nameBase) && sheetNameBase(key).equals(nameBase) + && nameBase.length() >= 2) + { + // e.g. catalog "DE express-general" -> sheet "DE express" + score = 70; + } + else if (StringUtils.isNotEmpty(codeKey) && key.contains(codeKey)) + { + score = 60; + } + if (score > bestScore) + { + bestScore = score; + best = workbook.getSheetAt(i); + } + } + return bestScore >= 60 ? best : null; + } + + private static String normalizeKey(String s) + { + if (s == null) + { + return ""; + } + String out = s.trim().toLowerCase(Locale.ROOT); + out = out.replace('\u2013', '-').replace('\u2014', '-').replace('\uFF0D', '-'); + out = out.replace(" ", "").replace("\u3000", ""); + return out; + } + + private static String sheetNameBase(String normalizedKey) + { + if (StringUtils.isEmpty(normalizedKey)) + { + return ""; + } + int idx = normalizedKey.lastIndexOf('-'); + if (idx <= 0) + { + return normalizedKey; + } + return normalizedKey.substring(0, idx); + } + + private List parsePriceSheet(Sheet sheet, BmsProduct product, + Map regionByNameCn, List euRegions, + Map zoneByCountryAndCode) + { + List list = new ArrayList(); + if (sheet == null) + { + return list; + } + int[] destHeader = findHeaderCell(sheet, "\u76ee\u7684\u5730"); + if (destHeader == null) + { + return list; + } + int headerRowIdx = destHeader[0]; + Row headerRow = sheet.getRow(headerRowIdx); + int colDest = destHeader[1]; + int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", colDest + 1); + int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", colDest + 1); + int colHandle = findColByHeader(headerRow, "\u64cd\u4f5c\u8d39", colDest + 1); + if (colHandle < 0) + { + colHandle = findColByHeader(headerRow, "\u6302\u53f7", colDest + 1); + } + int colLead = findColByHeader(headerRow, "\u65f6\u6548", colDest + 1); + int colFeature = findColByHeader(headerRow, "\u4ea7\u54c1\u7279\u6027", colDest + 1); + int colRemark = findColByHeader(headerRow, "\u5907\u6ce8", colDest + 1); + if (colWeight < 0) + { + colWeight = colDest + 2; + } + if (colFreight < 0) + { + colFreight = colDest + 3; + } + if (colHandle < 0) + { + colHandle = colDest + 4; + } + + String currentDest = null; + String currentLead = null; + String currentRemark = null; + int lastRow = sheet.getLastRowNum(); + for (int i = headerRowIdx + 1; i <= lastRow; i++) + { + Row row = sheet.getRow(i); + if (row == null) + { + continue; + } + String destCell = cellString(row.getCell(colDest)); + String weightLabel = cellString(row.getCell(colWeight)); + if ("\u76ee\u7684\u5730".equals(destCell) || (weightLabel != null && weightLabel.contains("\u91cd\u91cf\u6bb5"))) + { + continue; + } + if (StringUtils.isNotEmpty(destCell)) + { + currentDest = destCell; + currentLead = colLead >= 0 ? emptyToNull(cellString(row.getCell(colLead))) : null; + currentRemark = colRemark >= 0 ? emptyToNull(cellString(row.getCell(colRemark))) : null; + } + if (StringUtils.isEmpty(currentDest)) + { + continue; + } + if (currentDest.startsWith("\u4ea7\u54c1\u5b9a\u4e49") || currentDest.startsWith("\u4e00\u3001") + || currentDest.startsWith("\u4e8c\u3001") || currentDest.contains("\u670d\u52a1\u6761\u4ef6")) + { + break; + } + BigDecimal[] range = parseWeightRange(weightLabel); + if (range == null) + { + continue; + } + BigDecimal freight = cellDecimal(row.getCell(colFreight)); + BigDecimal handling = cellDecimal(row.getCell(colHandle)); + ZoneCodeAbbrevs.CountryZoneParts parts = ZoneCodeAbbrevs.splitCountryZone(currentDest); + if (parts == null || StringUtils.isEmpty(parts.countryName)) + { + continue; + } + String baseCountry = parts.countryName; + String zoneLabel = parts.zoneLabel; + String feature = colFeature >= 0 ? emptyToNull(cellString(row.getCell(colFeature))) : null; + String sizeOrRemark = feature != null ? feature : currentRemark; + + BmsRegion region = resolveRegion(baseCountry, regionByNameCn); + if (region != null) + { + list.add(buildRow(product, region.getCountryNameCn(), region.getCountryCode2(), zoneLabel, + zoneByCountryAndCode, range, freight, handling, sizeOrRemark, currentRemark, currentLead)); + continue; + } + if (isEuName(baseCountry) && StringUtils.isEmpty(zoneLabel)) + { + if (euRegions == null || euRegions.isEmpty()) + { + throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u89e3\u6790\u5230\u300c\u6b27\u76df\u300d\u4f46\u533a\u57df\u5e93\u65e0\u6b27\u76df\u56fd\u5bb6"); + } + for (BmsRegion eu : euRegions) + { + list.add(buildRow(product, eu.getCountryNameCn(), eu.getCountryCode2(), null, + zoneByCountryAndCode, range, freight, handling, sizeOrRemark, currentRemark, currentLead)); + } + continue; + } + list.add(buildRow(product, baseCountry, null, zoneLabel, zoneByCountryAndCode, range, freight, handling, + sizeOrRemark, currentRemark, currentLead)); + } + return list; + } + + private BmsProductCountry buildRow(BmsProduct product, String countryName, String countryCode, String zoneLabel, + Map zoneByCountryAndCode, BigDecimal[] range, BigDecimal freight, BigDecimal handling, + String sizeLimit, String lastMile, String leadTime) + { + BmsProductCountry c = new BmsProductCountry(); + if (product != null) + { + c.setProductCode(product.getProductCode()); + c.setProductName(product.getProductName()); + } + c.setCountryName(countryName); + c.setCountryCode(countryCode); + applyZone(c, countryCode, zoneLabel, zoneByCountryAndCode); + c.setWeightMin(range[0]); + c.setWeightMax(range[1]); + c.setFreightPrice(freight); + c.setRegistrationFee(handling); + c.setSizeLimit(sizeLimit); + c.setLastMile(lastMile); + c.setLeadTime(leadTime); + return c; + } + + private void applyZone(BmsProductCountry c, String countryCode, String zoneLabel, + Map zoneByCountryAndCode) + { + if (StringUtils.isEmpty(zoneLabel)) + { + c.setZoneId(null); + c.setZoneCode(null); + c.setZoneName(null); + return; + } + String zoneCode = ZoneCodeAbbrevs.toAbbrev(zoneLabel); + c.setZoneCode(zoneCode); + PrcZone zone = null; + if (StringUtils.isNotEmpty(countryCode) && zoneByCountryAndCode != null) + { + zone = zoneByCountryAndCode.get(countryCode.trim().toUpperCase(Locale.ROOT) + "|" + + zoneCode.trim().toUpperCase(Locale.ROOT)); + } + if (zone != null) + { + c.setZoneId(zone.getZoneId()); + c.setZoneCode(zone.getZoneCode()); + c.setZoneName(zone.getZoneName()); + } + else + { + c.setZoneId(null); + c.setZoneName(StringUtils.isNotEmpty(c.getCountryName()) + ? c.getCountryName() + "(" + zoneCode + ")" + : zoneCode); + } + } + + private BigDecimal[] parseWeightRange(String label) + { + if (StringUtils.isEmpty(label)) + { + return null; + } + String compact = label.replace(" ", "").replace("\u3000", ""); + Matcher m = WEIGHT_W_PATTERN.matcher(compact); + if (!m.find()) + { + m = WEIGHT_DASH_PATTERN.matcher(compact); + if (!m.find()) + { + return null; + } + } + try + { + BigDecimal min = new BigDecimal(m.group(1)); + BigDecimal max = new BigDecimal(m.group(2)); + if (min.compareTo(max) > 0) + { + BigDecimal tmp = min; + min = max; + max = tmp; + } + return new BigDecimal[] { min, max }; + } + catch (Exception e) + { + return null; + } + } + + private String dupCountryWeightTip(BmsProduct product, List rows) + { + if (rows == null || rows.isEmpty()) + { + return null; + } + Set seen = new HashSet(); + for (BmsProductCountry r : rows) + { + if (r == null) + { + continue; + } + String key = String.valueOf(r.getCountryCode()) + "|" + String.valueOf(r.getCountryName()) + + "|" + String.valueOf(r.getZoneCode()) + "|" + + (r.getWeightMin() == null ? "" : r.getWeightMin().toPlainString()) + "-" + + (r.getWeightMax() == null ? "" : r.getWeightMax().toPlainString()); + if (!seen.add(key)) + { + String name = product == null ? "" : product.getProductName(); + String code = product == null ? "" : product.getProductCode(); + return (StringUtils.isNotEmpty(name) ? name : code) + " \u91cd\u590d\u56fd\u5bb6+\u91cd\u91cf\u6bb5"; + } + } + return null; + } + + private Map loadRegionByNameCn() + { + BmsRegion query = new BmsRegion(); + query.setStatus("0"); + List regions = regionMapper.selectRegionList(query); + Map map = new HashMap(); + if (regions == null) + { + return map; + } + for (BmsRegion region : regions) + { + if (region == null || StringUtils.isEmpty(region.getCountryNameCn())) + { + continue; + } + map.put(region.getCountryNameCn().trim(), region); + } + return map; + } + + private List loadEuRegions(Map regionByNameCn) + { + List list = new ArrayList(); + if (regionByNameCn == null) + { + return list; + } + Set seen = new HashSet(); + for (BmsRegion region : regionByNameCn.values()) + { + if (region == null || !"1".equals(region.getIsEu())) + { + continue; + } + if (StringUtils.isEmpty(region.getCountryCode2())) + { + continue; + } + String code = region.getCountryCode2().toUpperCase(Locale.ROOT); + if (seen.add(code)) + { + list.add(region); + } + } + return list; + } + + private Map loadZoneByCountryAndCode() + { + Map map = new HashMap(); + List list = zoneMapper.selectPrcZoneList(new PrcZone()); + if (list == null) + { + return map; + } + for (PrcZone zone : list) + { + if (zone == null || StringUtils.isEmpty(zone.getZoneCode()) || StringUtils.isEmpty(zone.getCountryCode())) + { + continue; + } + map.put(zone.getCountryCode().trim().toUpperCase(Locale.ROOT) + "|" + + zone.getZoneCode().trim().toUpperCase(Locale.ROOT), zone); + } + return map; + } + + private BmsRegion resolveRegion(String excelName, Map regionByNameCn) + { + if (StringUtils.isEmpty(excelName) || regionByNameCn == null || regionByNameCn.isEmpty()) + { + return null; + } + String name = excelName.trim(); + BmsRegion exact = regionByNameCn.get(name); + if (exact != null) + { + return exact; + } + List candidates = new ArrayList(); + for (Map.Entry e : regionByNameCn.entrySet()) + { + String cn = e.getKey(); + if (cn == null) + { + continue; + } + if (cn.contains(name) || name.contains(cn)) + { + candidates.add(e.getValue()); + } + } + if (candidates.isEmpty()) + { + return null; + } + if (candidates.size() == 1) + { + return candidates.get(0); + } + BmsRegion best = candidates.get(0); + for (int i = 1; i < candidates.size(); i++) + { + String cn = candidates.get(i).getCountryNameCn(); + if (cn != null && cn.length() > best.getCountryNameCn().length()) + { + best = candidates.get(i); + } + } + return best; + } + + private static boolean isEuName(String name) + { + if (StringUtils.isEmpty(name)) + { + return false; + } + String n = name.trim(); + return "\u6b27\u76df".equals(n) || "EU".equalsIgnoreCase(n) || "\u6b27\u6d32\u8054\u76df".equals(n) + || "\u6b27\u76df\u56fd\u5bb6".equals(n) || "\u4e1c\u6b27".equals(n); + } + + private static int[] findHeaderCell(Sheet sheet, String headerText) + { + if (sheet == null || StringUtils.isEmpty(headerText)) + { + return null; + } + int lastRow = Math.min(sheet.getLastRowNum(), 30); + int[] containsHit = null; + for (int r = 0; r <= lastRow; r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + int maxCol = Math.min(Math.max(row.getLastCellNum(), 12), 20); + for (int c = 0; c < maxCol; c++) + { + String v = cellString(row.getCell(c)); + if (StringUtils.isEmpty(v)) + { + continue; + } + if (headerText.equals(v)) + { + return new int[] { r, c }; + } + if (containsHit == null && v.contains(headerText)) + { + containsHit = new int[] { r, c }; + } + } + } + return containsHit; + } + + private static int findColByHeader(Row headerRow, String keyword, int fromCol) + { + if (headerRow == null || StringUtils.isEmpty(keyword)) + { + return -1; + } + int maxCol = Math.min(Math.max(headerRow.getLastCellNum(), fromCol + 8), 20); + for (int c = Math.max(fromCol, 0); c < maxCol; c++) + { + String v = cellString(headerRow.getCell(c)); + if (StringUtils.isNotEmpty(v) && v.contains(keyword)) + { + return c; + } + } + return -1; + } + + private static String cellString(Cell cell) + { + if (cell == null) + { + return ""; + } + CellType type = cell.getCellType(); + if (type == CellType.FORMULA) + { + type = cell.getCachedFormulaResultType(); + } + switch (type) + { + case STRING: + return cell.getStringCellValue() == null ? "" : cell.getStringCellValue().trim(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) + { + Date d = cell.getDateCellValue(); + return d == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(d); + } + double num = cell.getNumericCellValue(); + if (Math.floor(num) == num) + { + return String.valueOf((long) num); + } + return String.valueOf(num); + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case BLANK: + return ""; + default: + return ""; + } + } + + private static BigDecimal cellDecimal(Cell cell) + { + if (cell == null) + { + return null; + } + CellType type = cell.getCellType(); + if (type == CellType.FORMULA) + { + type = cell.getCachedFormulaResultType(); + } + try + { + if (type == CellType.NUMERIC) + { + return BigDecimal.valueOf(cell.getNumericCellValue()); + } + String s = cellString(cell); + if (StringUtils.isEmpty(s)) + { + return null; + } + s = s.replace(",", "").replace("\uFFE5", "").replace("CNY", "").trim(); + if (StringUtils.isEmpty(s)) + { + return null; + } + return new BigDecimal(s); + } + catch (Exception e) + { + return null; + } + } + + private static String emptyToNull(String s) + { + return StringUtils.isEmpty(s) ? null : s; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java b/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java index 4ff075f8..f299467b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java @@ -247,7 +247,7 @@ public final class OrderBillExportSupport case 24: return "产品价未匹配国家"; case 25: return "产品价未匹配重量段"; case 4: return "未绑定套餐"; - case 5: return "无生效套餐版本"; + case 5: return "套餐未启用/无生效版本"; case 6: return "已生成应收账单"; case 7: return "产品档案缺失"; case 8: return "未命中价格"; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillLocalRebillAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillLocalRebillAsyncRunner.java index 98591ef9..4ca52106 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillLocalRebillAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillLocalRebillAsyncRunner.java @@ -6,13 +6,16 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -32,8 +35,9 @@ import com.bms.price.search.TabOrderSyncEsLog; import com.bms.price.service.IPrcOrderBillService; /** - * 仅根据 prc_order_bill 异步本地重计费(不走 Oracle / 不走 MQ)。 - * 大批量按 {@link #PAGE_SIZE} 分页处理;每一页在短生命周期线程池中执行。 + * Async local rebill from prc_order_bill only (no Oracle / MQ). + * Large jobs page by {@link #PAGE_SIZE}; each page runs in a short-lived pool. + * Cancel is cooperative: current in-flight bills finish, remaining bills are skipped. */ @Component public class PrcOrderBillLocalRebillAsyncRunner @@ -47,6 +51,11 @@ public class PrcOrderBillLocalRebillAsyncRunner public static final int MIN_THREADS = 2; public static final int MAX_THREADS = 16; private static final int FAIL_DETAIL_MAX = 200; + /** Publish Redis progress at most this often while a page is running. */ + private static final int PROGRESS_EVERY_N = 20; + private static final long PROGRESS_MIN_INTERVAL_MS = 500L; + + private final ConcurrentHashMap cancelFlags = new ConcurrentHashMap(); @Autowired @Lazy @@ -76,22 +85,36 @@ public class PrcOrderBillLocalRebillAsyncRunner int total = billIds == null ? 0 : billIds.size(); AtomicInteger success = new AtomicInteger(0); AtomicInteger fail = new AtomicInteger(0); + AtomicInteger done = new AtomicInteger(0); + AtomicLong lastProgressAt = new AtomicLong(0L); StringBuilder failTips = new StringBuilder(); List> failDetails = Collections.synchronizedList(new ArrayList>()); int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads()); + cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false)); ExecutorService pool = newWorkerPool(threads, "local-rebill"); try { put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, 1, "running", null, total, 0, 0); + lastProgressAt.set(System.currentTimeMillis()); if (total == 0) { put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", "no bills", 0, 0, 0); saveDoneLog(taskId, request, total, 0, 0, threads, failDetails, "FAIL", "no bills"); return; } - int processed = 0; + if (isCancelRequested(taskId)) + { + finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails); + return; + } + boolean cancelled = false; for (int from = 0; from < total; from += PAGE_SIZE) { + if (isCancelRequested(taskId)) + { + cancelled = true; + break; + } int to = Math.min(from + PAGE_SIZE, total); List page = billIds.subList(from, to); List> jobs = new ArrayList>(page.size()); @@ -99,6 +122,10 @@ public class PrcOrderBillLocalRebillAsyncRunner { final String one = billId; jobs.add(() -> { + if (isCancelRequested(taskId)) + { + return Boolean.FALSE; + } try { orderBillService.rebillLocalByBillId(one); @@ -114,14 +141,27 @@ public class PrcOrderBillLocalRebillAsyncRunner taskId, one, e.getMessage()); return Boolean.FALSE; } + finally + { + int finished = done.incrementAndGet(); + maybePublishProgress(taskId, total, finished, success.get(), fail.get(), lastProgressAt); + } }); } invokeAllQuiet(pool, jobs); - processed = to; - int percent = Math.min(99, (int) Math.floor(100.0 * processed / total)); - put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, percent, - "processing " + processed + "/" + total + " page=" + ((from / PAGE_SIZE) + 1), - null, total, success.get(), fail.get()); + if (isCancelRequested(taskId)) + { + cancelled = true; + break; + } + // Page boundary: always flush latest counts (even if throttle skipped mid-page). + publishProgress(taskId, total, done.get(), success.get(), fail.get()); + lastProgressAt.set(System.currentTimeMillis()); + } + if (cancelled || isCancelRequested(taskId)) + { + finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails); + return; } String msg = "success=" + success.get() + ", fail=" + fail.get(); String err = fail.get() > 0 && failTips.length() > 0 ? failTips.toString() : null; @@ -135,6 +175,11 @@ public class PrcOrderBillLocalRebillAsyncRunner } catch (Exception e) { + if (isCancelRequested(taskId)) + { + finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails); + return; + } log.error("local rebill aborted taskId={}", taskId, e); String msg = errorMessage(e); put(taskId, BatchAddIncomeBillFeeTask.STATUS_FAIL, 100, "failed", msg, @@ -144,10 +189,62 @@ public class PrcOrderBillLocalRebillAsyncRunner } finally { + cancelFlags.remove(taskId); shutdownPool(pool); } } + /** + * Request cooperative cancel. Returns current task snapshot (may still be RUNNING until page ends). + */ + public BatchAddIncomeBillFeeTask requestCancel(String taskId) + { + if (StringUtils.isEmpty(taskId)) + { + return null; + } + BatchAddIncomeBillFeeTask task = get(taskId); + if (task == null) + { + return null; + } + String st = task.getStatus(); + if (BatchAddIncomeBillFeeTask.STATUS_SUCCESS.equals(st) + || BatchAddIncomeBillFeeTask.STATUS_FAIL.equals(st) + || BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(st)) + { + return task; + } + cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false)).set(true); + // Keep RUNNING so clients keep polling until finishCancelled writes CANCELLED. + put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, + Math.max(task.getPercent(), 1), + "cancelling", + null, + task.getTotal(), task.getSuccess(), task.getFail()); + log.info("local rebill cancel requested taskId={} success={} fail={} total={}", + taskId, task.getSuccess(), task.getFail(), task.getTotal()); + return get(taskId); + } + + private void finishCancelled(String taskId, LocalRebillRequest request, int total, int ok, int fail, + int threads, List> failDetails) + { + int percent = total <= 0 ? 100 : Math.min(100, (int) Math.floor(100.0 * (ok + fail) / total)); + String msg = "cancelled success=" + ok + ", fail=" + fail; + put(taskId, BatchAddIncomeBillFeeTask.STATUS_CANCELLED, Math.max(percent, 1), msg, + "cancelled by user", total, ok, fail); + saveDoneLog(taskId, request, total, ok, fail, threads, failDetails, + EsBizLogSupport.STATUS_FAIL, "cancelled by user"); + log.info("local rebill cancelled taskId={} total={} success={} fail={}", taskId, total, ok, fail); + } + + private boolean isCancelRequested(String taskId) + { + AtomicBoolean flag = cancelFlags.get(taskId); + return flag != null && flag.get(); + } + private void saveDoneLog(String taskId, LocalRebillRequest request, int total, int ok, int fail, int threads, List> failDetails, String status, String errorMessage) { @@ -209,6 +306,24 @@ public class PrcOrderBillLocalRebillAsyncRunner public void put(String taskId, String status, int percent, String message, String error, int total, int success, int fail) { + BatchAddIncomeBillFeeTask existing = get(taskId); + if (existing != null + && BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(existing.getStatus()) + && !BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(status)) + { + // Keep cancelled: do not let a late RUNNING overwrite after user cancel. + BatchAddIncomeBillFeeTask keep = new BatchAddIncomeBillFeeTask(); + keep.setTaskId(taskId); + keep.setStatus(BatchAddIncomeBillFeeTask.STATUS_CANCELLED); + keep.setPercent(Math.max(existing.getPercent(), percent)); + keep.setMessage(existing.getMessage()); + keep.setError(existing.getError()); + keep.setTotal(total > 0 ? total : existing.getTotal()); + keep.setSuccess(Math.max(existing.getSuccess(), success)); + keep.setFail(Math.max(existing.getFail(), fail)); + redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, keep, TTL_MINUTES, TimeUnit.MINUTES); + return; + } BatchAddIncomeBillFeeTask task = new BatchAddIncomeBillFeeTask(); task.setTaskId(taskId); task.setStatus(status); @@ -226,6 +341,43 @@ public class PrcOrderBillLocalRebillAsyncRunner return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId); } + /** + * Throttled Redis progress so the UI moves during a large page (not only after 500). + * Does not change worker throughput; only how often status is published. + */ + private void maybePublishProgress(String taskId, int total, int finished, int ok, int fail, + AtomicLong lastProgressAt) + { + if (total <= 0 || finished <= 0) + { + return; + } + boolean byCount = finished % PROGRESS_EVERY_N == 0 || finished >= total; + long now = System.currentTimeMillis(); + long prev = lastProgressAt.get(); + boolean byTime = now - prev >= PROGRESS_MIN_INTERVAL_MS; + if (!byCount && !byTime) + { + return; + } + if (!lastProgressAt.compareAndSet(prev, now) && !byCount) + { + return; + } + publishProgress(taskId, total, finished, ok, fail); + } + + private void publishProgress(String taskId, int total, int finished, int ok, int fail) + { + int percent = total <= 0 ? 0 : Math.min(99, (int) Math.floor(100.0 * finished / total)); + if (finished >= total) + { + percent = Math.min(99, percent); + } + put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING, percent, + "processing " + finished + "/" + total, null, total, ok, fail); + } + private static void invokeAllQuiet(ExecutorService pool, List> jobs) throws InterruptedException { List> futures = pool.invokeAll(jobs); diff --git a/bms-back/bms-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml b/bms-back/bms-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml index fbbf8b95..8f35acbb 100644 --- a/bms-back/bms-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/customer/BmsCustomerMapper.xml @@ -11,7 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + @@ -45,7 +45,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select c.customer_id, c.customer_code, c.customer_name, c.customer_code_dw, c.credit_code, c.customer_type, c.area_type, + select c.customer_id, c.customer_code, c.customer_name, c.customer_code_dw, c.credit_code, c.customer_type, c.customer_area, c.is_enable, case when su.user_id is not null then cast(su.user_id as char) else null end as sales_user_id, su.nick_name as sales_user, c.ed, c.zq, c.settlement_user_id, c.settlement_user, @@ -92,7 +92,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and c.credit_code = #{creditCode} and c.customer_type = #{customerType} - and c.area_type = #{areaType} + and c.customer_area = #{customerArea} and c.is_enable = #{isEnable} and c.sales_user_id = #{salesUserId} and su.nick_name = #{salesUser} @@ -317,7 +317,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" customer_code_dw, credit_code, customer_type, - area_type, + customer_area, is_enable, sales_user_id, sales_user, @@ -350,7 +350,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{customerCodeDw}, #{creditCode}, #{customerType}, - #{areaType}, + #{customerArea}, #{isEnable}, #{salesUserId}, #{salesUser}, @@ -387,7 +387,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" customer_code_dw = #{customerCodeDw}, credit_code = #{creditCode}, customer_type = #{customerType}, - area_type = #{areaType}, + customer_area = #{customerArea}, is_enable = #{isEnable}, sales_user_id = #{salesUserId}, sales_user = #{salesUser}, diff --git a/bms-back/bms-system/src/main/resources/mapper/customer/CustomerSyncMapper.xml b/bms-back/bms-system/src/main/resources/mapper/customer/CustomerSyncMapper.xml index 6bd7222d..44913610 100644 --- a/bms-back/bms-system/src/main/resources/mapper/customer/CustomerSyncMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/customer/CustomerSyncMapper.xml @@ -30,6 +30,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" del_flag, create_by, sales_user_id, source_sales_code , customer_code_dw , customer_type + , customer_area , sales_user , settlement_user , service_user @@ -46,6 +47,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" '0', #{createBy}, #{salesUserId}, #{sourceSalesCode} , #{customerCodeDw} , #{customerType} + , #{customerArea} , #{salesUser} , #{settlementUser} , #{serviceUser} @@ -66,6 +68,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" , customer_name = #{customerName} , customer_code_dw = #{customerCodeDw} , customer_type = #{customerType} + , customer_area = #{customerArea} , sales_user_id = #{salesUserId} , source_sales_code = #{sourceSalesCode} , sales_user = #{salesUser} diff --git a/bms-back/bms-system/src/main/resources/mapper/customer/OracleCustomerSyncMapper.xml b/bms-back/bms-system/src/main/resources/mapper/customer/OracleCustomerSyncMapper.xml index 0aad5a20..ecd510ed 100644 --- a/bms-back/bms-system/src/main/resources/mapper/customer/OracleCustomerSyncMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/customer/OracleCustomerSyncMapper.xml @@ -10,7 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" BELONG_SALER, BELONG_SALER_NAME, SETTLEMENT_MAN, SERVICE_MAN, REMARK, WEIGHT_LIMIT, CREDIT_LIMIT, TEMP_AMOUNT, ACCOUNT_AMOUNT, EUR_AMOUNT, - CUSTOMER_TYPE, CUSTOMER_BALANCE_MODE, BL_OPEN, + CUSTOMER_TYPE, CUSTOMER_AREA, CUSTOMER_BALANCE_MODE, BL_OPEN, CREATE_DATE, GMT_CREATE FROM WEISHI_USER.TAB_CUSTOMER diff --git a/bms-back/bms-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml index 55796df3..9f8e7b6c 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml @@ -66,7 +66,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND o.PICKUP_DATE <= TO_DATE(#{query.endTime}, 'YYYY-MM-DD HH24:MI:SS') - AND o.BL_PICKUP = 1 + + + AND o.BL_PICKUP = #{query.blPickup} + + + + + AND o.ORDER_STATUS IN @@ -123,8 +130,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" o.TRACK_ID, o.CUSTOMER_CODE, o.CUSTOMER_NAME, - o.PRODUCT_CODE, - o.PRODUCT_NAME, + + o.LINE_CODE AS PRODUCT_CODE, + o.LINE_NAME AS PRODUCT_NAME, o.PRODUCT_FLOOR_CODE, o.COUNTRY, o.COUNTRY_NAME, @@ -273,7 +281,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{bid} - AND o.BL_PICKUP = 1 + + + AND o.BL_PICKUP = #{blPickup} + + + + + AND o.ORDER_STATUS IN (3, 4, 5, 6, 7, 8, 11) AND o.CUSTOMER_CODE IN diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml index 2ae1ed96..ca58274a 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml @@ -519,6 +519,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" limit 1 + + + select s.price_set_id, s.price_set_code, s.price_set_name, s.price_set_note, - s.set_type, s.default_discount, s.default_type, s.calculate_sign, + s.set_type, ifnull(s.set_category, 'PACKAGE') as set_category, + s.default_discount, s.default_type, s.calculate_sign, s.status, s.tms_id, s.created_by, s.create_time, s.updated_by, s.update_time, null as product_count, null as customer_count, null as bound_customers @@ -66,6 +69,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND s.set_type = #{setType} + + AND ifnull(s.set_category, 'PACKAGE') = #{setCategory} + AND s.status = #{status} @@ -126,6 +132,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND tms_id = #{tmsId} + + AND ifnull(set_category, 'PACKAGE') = #{setCategory} + @@ -134,6 +143,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" price_set_name, price_set_note, set_type, + set_category, default_discount, default_type, calculate_sign, @@ -146,6 +156,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{priceSetName}, #{priceSetNote}, #{setType}, + #{setCategory}, #{defaultDiscount}, #{defaultType}, #{calculateSign}, @@ -163,6 +174,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" price_set_name = #{priceSetName}, price_set_note = #{priceSetNote}, set_type = #{setType}, + set_category = #{setCategory}, default_discount = #{defaultDiscount}, default_type = #{defaultType}, calculate_sign = #{calculateSign}, diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PrcProfitReportMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PrcProfitReportMapper.xml index 3320fc3d..ac667444 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PrcProfitReportMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PrcProfitReportMapper.xml @@ -119,6 +119,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from prc_income_bill i inner join prc_order_bill p on p.bill_id = i.bill_id + + left join ctr_customer c on c.customer_id = p.customer_id and c.del_flag = '0' + and (p.billing_fail_status is null or p.billing_fail_status not in (1, 2, 3)) + + and c.customer_area = #{customerArea} + and p.biz_date >= #{beginBizDate} @@ -194,8 +200,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + + + i.sales_amount is not null and i.redline_amount is not null + and not (coalesce(p.billing_fail_status, 0) in (23, 24, 25) and i.sales_amount = 0 and i.redline_amount = 0) + + + + + null as cost_amount, i.redline_amount, + null as redline_gross_profit, null as redline_gross_margin_rate, + i.sales_amount, + case when + then i.sales_amount - i.redline_amount else null end as sales_gross_profit, + case when and i.sales_amount != 0 + then round((i.sales_amount - i.redline_amount) * 100 / i.sales_amount, 4) + else null end as sales_gross_margin_rate + + i.product_amount as cost_amount, i.redline_amount, case when i.product_amount is not null and i.redline_amount is not null @@ -207,11 +230,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" then i.sales_amount - i.product_amount else null end as sales_gross_profit, case when i.product_amount is not null and i.sales_amount is not null and i.sales_amount != 0 then round((i.sales_amount - i.product_amount) * 100 / i.sales_amount, 4) else null end as sales_gross_margin_rate - + + - + select p.product_code, p.product_name, p.country_code, p.country_name, + i.currency, count(*) as order_count, + group by p.product_code, p.product_name, p.country_code, p.country_name, i.currency order by p.product_code, p.country_code, i.currency @@ -273,41 +314,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"