From 308526ecee9de070e77ce817c26e205bb4af6237 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Thu, 5 Nov 2026 12:57:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=93=E5=B1=9E=E6=8A=A5?= =?UTF-8?q?=E4=BB=B7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../price/ExclusiveQuoteController.java | 453 +++++++++ .../controller/price/PriceSetController.java | 11 + .../com/bms/price/domain/PrcPriceSet.java | 7 + .../bms/price/service/IBmsProductService.java | 4 - .../price/service/IPrcPriceSetService.java | 10 + .../service/impl/BmsProductServiceImpl.java | 20 +- .../service/impl/PrcPriceSetServiceImpl.java | 95 +- .../ExclusiveHalaraQuoteExcelParser.java | 913 ++++++++++++++++++ .../mapper/price/PrcPriceSetMapper.xml | 16 +- bms-back/sql/bms_init.sql | 2 + bms-back/sql/price/exclusive_quote_alter.sql | 32 + bms-back/sql/price/exclusive_quote_menu.sql | 59 ++ bms-front/src/api/price/createPriceSetApi.js | 484 ++++++++++ bms-front/src/api/price/exclusive.js | 43 + bms-front/src/api/price/set.js | 479 +-------- .../src/views/price/exclusive/create.vue | 565 +++++++++++ .../src/views/price/exclusive/detail.vue | 538 +++++++++++ .../views/price/exclusive/exclusiveQuote.vue | 297 ++++++ .../set/components/create/PriceEditPanel.vue | 12 +- .../set/components/create/StepDetails.vue | 61 +- .../components/detail/SetCustomerDialog.vue | 8 +- .../components/detail/SetCustomerTable.vue | 13 +- .../set/components/detail/SetDetailDialog.vue | 6 +- .../set/components/detail/SetDetailHeader.vue | 10 +- .../set/components/detail/SetDetailTable.vue | 13 +- .../detail/SetVersionDetailDrawer.vue | 8 +- .../detail/SetVersionExcelFileDrawer.vue | 6 +- .../set/components/detail/SetVersionTable.vue | 17 +- .../set/composables/usePriceSetModule.js | 77 ++ bms-front/src/views/price/set/create.vue | 30 +- bms-front/src/views/price/set/detail.vue | 24 +- bms-front/src/views/price/set/priceSet.vue | 71 +- 32 files changed, 3817 insertions(+), 567 deletions(-) create mode 100644 bms-back/bms-admin/src/main/java/com/bms/web/controller/price/ExclusiveQuoteController.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/service/support/ExclusiveHalaraQuoteExcelParser.java create mode 100644 bms-back/sql/price/exclusive_quote_alter.sql create mode 100644 bms-back/sql/price/exclusive_quote_menu.sql create mode 100644 bms-front/src/api/price/createPriceSetApi.js create mode 100644 bms-front/src/api/price/exclusive.js create mode 100644 bms-front/src/views/price/exclusive/create.vue create mode 100644 bms-front/src/views/price/exclusive/detail.vue create mode 100644 bms-front/src/views/price/exclusive/exclusiveQuote.vue create mode 100644 bms-front/src/views/price/set/composables/usePriceSetModule.js 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-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/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/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/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/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/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/resources/mapper/price/PrcPriceSetMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetMapper.xml index ba21255b..59750c4b 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetMapper.xml @@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -26,7 +27,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 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, (select count(distinct p.product_code) @@ -52,7 +54,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" @@ -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/sql/bms_init.sql b/bms-back/sql/bms_init.sql index 02c3ee87..c3aa62ea 100644 --- a/bms-back/sql/bms_init.sql +++ b/bms-back/sql/bms_init.sql @@ -1220,6 +1220,7 @@ CREATE TABLE `prc_price_set` ( `price_set_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '报价名称', `price_set_note` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', `set_type` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'FIXED' COMMENT '计价类型 FIXED=固定价 DISCOUNT=折扣', + `set_category` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PACKAGE' COMMENT '分类 PACKAGE=普通套餐 EXCLUSIVE=专属报价', `default_discount` decimal(5,2) DEFAULT NULL COMMENT '默认折扣百分数 1-99,仅 DISCOUNT', `default_type` char(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '默认套餐 Y=默认', `calculate_sign` char(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '试算标记 Y=外部试算套餐', @@ -1233,6 +1234,7 @@ CREATE TABLE `prc_price_set` ( UNIQUE KEY `uk_price_set_code` (`price_set_code`), KEY `idx_ps_status` (`status`), KEY `idx_ps_type` (`set_type`), + KEY `idx_ps_category` (`set_category`), KEY `idx_ps_tms` (`tms_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='价格方案表-报价基本资料'; diff --git a/bms-back/sql/price/exclusive_quote_alter.sql b/bms-back/sql/price/exclusive_quote_alter.sql new file mode 100644 index 00000000..7729941e --- /dev/null +++ b/bms-back/sql/price/exclusive_quote_alter.sql @@ -0,0 +1,32 @@ +-- Exclusive quote: share prc_price_set via set_category +SET NAMES utf8mb4; + +SET @col_exists := ( + SELECT COUNT(1) FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'prc_price_set' + AND column_name = 'set_category' +); +SET @sql := IF(@col_exists = 0, + 'ALTER TABLE prc_price_set ADD COLUMN set_category varchar(20) NOT NULL DEFAULT ''PACKAGE'' COMMENT ''分类 PACKAGE=普通套餐 EXCLUSIVE=专属报价'' AFTER set_type', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +UPDATE prc_price_set + SET set_category = 'PACKAGE' + WHERE set_category IS NULL OR set_category = ''; + +SET @idx_exists := ( + SELECT COUNT(1) FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'prc_price_set' + AND index_name = 'idx_ps_category' +); +SET @sql := IF(@idx_exists = 0, + 'ALTER TABLE prc_price_set ADD KEY idx_ps_category (set_category)', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/bms-back/sql/price/exclusive_quote_menu.sql b/bms-back/sql/price/exclusive_quote_menu.sql new file mode 100644 index 00000000..e6e831ee --- /dev/null +++ b/bms-back/sql/price/exclusive_quote_menu.sql @@ -0,0 +1,59 @@ +-- Exclusive quote module menus +SET NAMES utf8mb4; + +-- Top-level directory +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2600, N'专属报价', 0, 55, 'exclusive', NULL, '', '', 1, 0, 'M', '0', '0', '', 'money', 'admin', sysdate(), N'老板客户专属报价' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2600 OR (path = 'exclusive' AND parent_id = 0 AND menu_type = 'M')); + +SET @exParentId := (SELECT menu_id FROM sys_menu WHERE menu_id = 2600 LIMIT 1); +SET @exParentId := IFNULL(@exParentId, ( + SELECT menu_id FROM sys_menu WHERE path = 'exclusive' AND menu_type = 'M' AND parent_id = 0 ORDER BY menu_id ASC LIMIT 1 +)); + +-- List page +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2601, N'报价列表', @exParentId, 1, 'quote', 'price/exclusive/exclusiveQuote', '', 'ExclusiveQuote', 1, 0, 'C', '0', '0', 'price:exclusive:list', 'list', 'admin', sysdate(), N'专属报价列表' +FROM DUAL WHERE @exParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2601 OR perms = 'price:exclusive:list'); + +-- Hidden create / detail +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2609, N'报价新建', @exParentId, 2, 'quoteCreate', 'price/exclusive/create', '', 'ExclusiveQuoteCreate', 1, 0, 'C', '1', '0', 'price:exclusive:add', '#', 'admin', sysdate(), N'专属报价新建/编辑' +FROM DUAL WHERE @exParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2609 OR path = 'quoteCreate'); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2610, N'报价详情', @exParentId, 3, 'quoteDetail', 'price/exclusive/detail', '', 'ExclusiveQuoteDetail', 1, 0, 'C', '1', '0', 'price:exclusive:query', '#', 'admin', sysdate(), N'专属报价详情' +FROM DUAL WHERE @exParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2610 OR path = 'quoteDetail'); + +-- Buttons under list +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2602, N'查询', 2601, 1, '', NULL, 1, 0, 'F', '0', '0', 'price:exclusive:query', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2602 OR (parent_id = 2601 AND perms = 'price:exclusive:query' AND menu_type = 'F')); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2603, N'新增', 2601, 2, '', NULL, 1, 0, 'F', '0', '0', 'price:exclusive:add', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2603 OR (parent_id = 2601 AND perms = 'price:exclusive:add' AND menu_type = 'F')); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2604, N'修改', 2601, 3, '', NULL, 1, 0, 'F', '0', '0', 'price:exclusive:edit', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2604 OR (parent_id = 2601 AND perms = 'price:exclusive:edit' AND menu_type = 'F')); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2605, N'删除', 2601, 4, '', NULL, 1, 0, 'F', '0', '0', 'price:exclusive:remove', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2605 OR (parent_id = 2601 AND perms = 'price:exclusive:remove' AND menu_type = 'F')); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2606, N'启停/审核', 2601, 5, '', NULL, 1, 0, 'F', '0', '0', 'price:exclusive:audit', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2606 OR (parent_id = 2601 AND perms = 'price:exclusive:audit' AND menu_type = 'F')); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2607, N'强制改价', 2601, 6, '', NULL, 1, 0, 'F', '0', '0', 'price:exclusive:forceEdit', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2607 OR (parent_id = 2601 AND perms = 'price:exclusive:forceEdit' AND menu_type = 'F')); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id FROM ( + SELECT 2600 AS menu_id UNION SELECT 2601 UNION SELECT 2602 UNION SELECT 2603 + UNION SELECT 2604 UNION SELECT 2605 UNION SELECT 2606 UNION SELECT 2607 + UNION SELECT 2609 UNION SELECT 2610 +) m +WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); diff --git a/bms-front/src/api/price/createPriceSetApi.js b/bms-front/src/api/price/createPriceSetApi.js new file mode 100644 index 00000000..948f45a0 --- /dev/null +++ b/bms-front/src/api/price/createPriceSetApi.js @@ -0,0 +1,484 @@ +import request from '@/utils/request' + +/** Build price-set style API against a controller base path (e.g. /price/set, /price/exclusive). */ +export function createPriceSetApi(basePath) { + const base = String(basePath || '/price/set').replace(/\/$/, '') + function listPriceSet(query) { + return request({ + url: base + '/list', + method: 'get', + params: query + }) + } + + function getPriceSet(priceSetId) { + return request({ + url: base + '/' + priceSetId, + method: 'get' + }) + } + + function savePriceSet(data) { + return request({ + url: base + '', + method: 'post', + data: data + }) + } + + function getPriceSetSaveTask(taskId) { + return request({ + url: base + '/save/task/' + taskId, + method: 'get', + timeout: 10 * 60 * 1000 + }) + } + + /** + * Submit save async and poll Redis progress until SUCCESS/FAIL. + * @returns {Promise<{ priceSetId: number|string, task: object }>} + */ + async function savePriceSetAsync(data, onProgress) { + const submitRes = await savePriceSet(data) + const taskId = pickSaveTaskId(submitRes) + if (!taskId) { + throw new Error('missing save taskId') + } + const started = Date.now() + const maxMs = 30 * 60 * 1000 + while (Date.now() - started < maxMs) { + const res = await getPriceSetSaveTask(taskId) + const task = res.data || {} + if (typeof onProgress === 'function') { + onProgress(task) + } + if (task.status === 'SUCCESS') { + return { priceSetId: task.priceSetId, task } + } + if (task.status === 'FAIL') { + const err = new Error(task.error || task.message || 'save failed') + err.task = task + throw err + } + await new Promise(resolve => setTimeout(resolve, 400)) + } + throw new Error('save timeout') + } + + /** Accept data.taskId / data string / legacy msg UUID (Bms String overload). */ + function pickSaveTaskId(res) { + if (!res) return null + const d = res.data + if (d && typeof d === 'object' && d.taskId) return String(d.taskId) + if (typeof d === 'string' && d.trim()) return d.trim() + if (typeof res.msg === 'string') { + const m = res.msg.trim() + if (/^[a-f0-9]{32}$/i.test(m)) return m + if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(m)) { + return m.replace(/-/g, '') + } + } + return null + } + + function updatePriceSet(data) { + return request({ + url: base + '', + method: 'put', + data: data + }) + } + + /** Update package name and note only. */ + function updatePriceSetBasic(data) { + return request({ + url: base + '/basic', + method: 'put', + data: data + }) + } + + function delPriceSet(priceSetIds) { + return request({ + url: base + '/' + priceSetIds, + method: 'delete' + }) + } + + function submitPriceSet(priceSetId) { + return request({ + url: base + '/submit/' + priceSetId, + method: 'put' + }) + } + + function approvePriceSet(priceSetId) { + return request({ + url: base + '/approve/' + priceSetId, + method: 'put' + }) + } + + function rejectPriceSet(priceSetId) { + return request({ + url: base + '/reject/' + priceSetId, + method: 'put' + }) + } + + function activatePriceSetVersion(psvId, data) { + return request({ + url: base + '/version/activate/' + psvId, + method: 'put', + data: data || {} + }) + } + + function copyPriceSetVersion(psvId) { + return request({ + url: base + '/version/copy/' + psvId, + method: 'post' + }) + } + + /** Create empty draft version under package */ + function createPriceSetVersion(priceSetId) { + return request({ + url: base + '/version/create/' + priceSetId, + method: 'post' + }) + } + + /** Import excel attachments for a price-set version (via prc_price_set_product.psv_id). */ + function listPriceSetVersionExcelFiles(psvId) { + return request({ + url: base + '/version/' + psvId + '/excelFiles', + method: 'get' + }) + } + + function deletePriceSetVersion(psvId) { + return request({ + url: base + '/version/' + psvId, + method: 'delete' + }) + } + + function disablePriceSet(priceSetId) { + return request({ + url: base + '/disable/' + priceSetId, + method: 'put' + }) + } + + function enablePriceSet(priceSetId) { + return request({ + url: base + '/enable/' + priceSetId, + method: 'put' + }) + } + + function copyPriceSet(priceSetId) { + return request({ + url: base + '/copy/' + priceSetId, + method: 'post' + }) + } + + function validatePriceSet(data) { + return request({ + url: base + '/validate', + method: 'post', + data: data + }) + } + + function listSetDetail(query) { + return request({ + url: base + '/detail/list', + method: 'get', + params: query + }) + } + + /** All detail rows for one product under a version (no pagination). */ + function listSetDetailByProduct(psvId, productCode) { + return request({ + url: base + '/detail/byProduct', + method: 'get', + params: { psvId, productCode } + }) + } + + /** All detail rows for a version (no pagination); one call instead of N byProduct. */ + function listSetDetailByVersion(psvId) { + return request({ + url: base + '/detail/byVersion', + method: 'get', + params: { psvId } + }) + } + + /** 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 */ + function listSetDetailOptions(psvId) { + return request({ + url: base + '/detail/options', + method: 'get', + params: { psvId } + }) + } + + function addSetDetail(data) { + return request({ + url: base + '/detail', + method: 'post', + data: data + }) + } + + + /** + * Async import quote Excel into FIXED package detail prices; poll until done. + * @param {File|Blob} file + * @param {{ priceSetId: string|number, psvId?: string|number, updateOnly?: boolean, replaceByProductCountry?: boolean, countryCode?: string }} query + * @param {(task: object) => void} [onProgress] + * @returns {Promise<{ task: object }>} + */ + function appendUploadFiles(formData, file) { + const list = Array.isArray(file) ? file : [file] + for (const item of list) { + const raw = item && (item.raw || item) + if (raw) formData.append('files', raw) + } + } + + async function importSetDetailPricesAsync(file, query, onProgress) { + const formData = new FormData() + appendUploadFiles(formData, file) + const params = new URLSearchParams() + if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId) + if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId) + if (query && query.updateOnly) params.set('updateOnly', 'true') + // Package Excel build-price only (product+country replace scope) + if (query && query.replaceByProductCountry) params.set('replaceByProductCountry', 'true') + if (query && query.countryCode) params.set('countryCode', query.countryCode) + const submitRes = await request({ + url: base + '/detail/importPrices?' + params.toString(), + method: 'post', + data: formData, + headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false } + }) + const taskId = pickSaveTaskId(submitRes) + if (!taskId) { + throw new Error('missing import taskId') + } + return pollPriceSetSaveTask(taskId, onProgress) + } + + /** + * Preview pull product library (no DB write). Returns { products, details, message }. + */ + function previewPullFromLibrary() { + return request({ + url: base + '/detail/previewPullFromLibrary', + method: 'post' + }) + } + + /** + * Preview Excel import (no DB write). + * @param {File|Blob} file + * @param {{ updateOnly?: boolean, replaceByProductCountry?: boolean, countryCode?: string, details?: Array }} [opts] + */ + function previewImportDetailPrices(file, opts = {}) { + const formData = new FormData() + appendUploadFiles(formData, file) + if (opts.updateOnly) formData.append('updateOnly', 'true') + if (opts.replaceByProductCountry) formData.append('replaceByProductCountry', 'true') + if (opts.countryCode) formData.append('countryCode', opts.countryCode) + // updateOnly / Excel build-price merge need current draft details + if ((opts.updateOnly || opts.replaceByProductCountry || opts.countryCode) && Array.isArray(opts.details)) { + formData.append('detailsJson', JSON.stringify(opts.details)) + } + return request({ + url: base + '/detail/previewImportPrices', + method: 'post', + data: formData, + // FormData must not go through repeat-submit JSON.stringify (collapses to "{}") + headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }, + timeout: 10 * 60 * 1000 + }) + } + + /** + * Async pull product library into package version details; poll until done. + * @param {{ priceSetId: string|number, psvId?: string|number, force?: boolean }} query + * @param {(task: object) => void} [onProgress] + */ + async function pullSetDetailsFromLibraryAsync(query, onProgress) { + const params = new URLSearchParams() + if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId) + if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId) + params.set('force', query && query.force ? 'true' : 'false') + const submitRes = await request({ + url: base + '/detail/pullFromLibrary?' + params.toString(), + method: 'post' + }) + const taskId = pickSaveTaskId(submitRes) + if (!taskId) { + throw new Error('missing pull taskId') + } + return pollPriceSetSaveTask(taskId, onProgress) + } + + async function pollPriceSetSaveTask(taskId, onProgress) { + const started = Date.now() + const maxMs = 30 * 60 * 1000 + while (Date.now() - started < maxMs) { + const res = await getPriceSetSaveTask(taskId) + const task = res.data || {} + if (typeof onProgress === 'function') { + onProgress(task) + } + if (task.status === 'SUCCESS') { + return { task } + } + if (task.status === 'FAIL') { + const err = new Error(task.error || task.message || 'task failed') + err.task = task + throw err + } + await new Promise(resolve => setTimeout(resolve, 400)) + } + throw new Error('task timeout') + } + + function updateSetDetail(data) { + return request({ + url: base + '/detail', + method: 'put', + data: data + }) + } + + /** + * Submit async batch write of detail surcharge snapshots and poll until done. + * @returns {Promise<{ task: object }>} + */ + async function updateSetDetailSurchargeAsync(details, onProgress) { + const submitRes = await request({ + url: base + '/detail/surcharge/async', + method: 'post', + data: details || [] + }) + const taskId = pickSaveTaskId(submitRes) + if (!taskId) { + throw new Error('missing surcharge taskId') + } + const started = Date.now() + const maxMs = 30 * 60 * 1000 + while (Date.now() - started < maxMs) { + const res = await getPriceSetSaveTask(taskId) + const task = res.data || {} + if (typeof onProgress === 'function') { + onProgress(task) + } + if (task.status === 'SUCCESS') { + return { task } + } + if (task.status === 'FAIL') { + const err = new Error(task.error || task.message || 'surcharge save failed') + err.task = task + throw err + } + await new Promise(resolve => setTimeout(resolve, 400)) + } + throw new Error('surcharge save timeout') + } + + function delSetDetail(psdIds) { + return request({ + url: base + '/detail/' + psdIds, + method: 'delete' + }) + } + + function listSetCustomer(query) { + return request({ + url: base + '/customer/list', + method: 'get', + params: query + }) + } + + function addSetCustomer(data) { + return request({ + url: base + '/customer', + method: 'post', + data: data + }) + } + + function batchAddSetCustomer(data) { + return request({ + url: base + '/customer/batch', + method: 'post', + data: data + }) + } + + function updateSetCustomer(data) { + return request({ + url: base + '/customer', + method: 'put', + data: data + }) + } + + function delSetCustomer(ids) { + return request({ + url: base + '/customer/' + ids, + method: 'delete' + }) + } + + return { + listPriceSet, + getPriceSet, + savePriceSet, + getPriceSetSaveTask, + savePriceSetAsync, + updatePriceSet, + updatePriceSetBasic, + delPriceSet, + submitPriceSet, + approvePriceSet, + rejectPriceSet, + activatePriceSetVersion, + copyPriceSetVersion, + createPriceSetVersion, + listPriceSetVersionExcelFiles, + deletePriceSetVersion, + disablePriceSet, + enablePriceSet, + copyPriceSet, + validatePriceSet, + listSetDetail, + listSetDetailByProduct, + listSetDetailByVersion, + listSetDetailOptions, + addSetDetail, + importSetDetailPricesAsync, + previewPullFromLibrary, + previewImportDetailPrices, + pullSetDetailsFromLibraryAsync, + updateSetDetail, + updateSetDetailSurchargeAsync, + delSetDetail, + listSetCustomer, + addSetCustomer, + batchAddSetCustomer, + updateSetCustomer, + delSetCustomer + } +} diff --git a/bms-front/src/api/price/exclusive.js b/bms-front/src/api/price/exclusive.js new file mode 100644 index 00000000..1d03052f --- /dev/null +++ b/bms-front/src/api/price/exclusive.js @@ -0,0 +1,43 @@ +import { createPriceSetApi } from './createPriceSetApi' + +export const exclusiveQuoteApi = createPriceSetApi('/price/exclusive') + +export const { + listPriceSet, + getPriceSet, + savePriceSet, + getPriceSetSaveTask, + savePriceSetAsync, + updatePriceSet, + updatePriceSetBasic, + delPriceSet, + submitPriceSet, + approvePriceSet, + rejectPriceSet, + activatePriceSetVersion, + copyPriceSetVersion, + createPriceSetVersion, + listPriceSetVersionExcelFiles, + deletePriceSetVersion, + disablePriceSet, + enablePriceSet, + copyPriceSet, + validatePriceSet, + listSetDetail, + listSetDetailByProduct, + listSetDetailByVersion, + listSetDetailOptions, + addSetDetail, + importSetDetailPricesAsync, + previewPullFromLibrary, + previewImportDetailPrices, + pullSetDetailsFromLibraryAsync, + updateSetDetail, + updateSetDetailSurchargeAsync, + delSetDetail, + listSetCustomer, + addSetCustomer, + batchAddSetCustomer, + updateSetCustomer, + delSetCustomer +} = exclusiveQuoteApi diff --git a/bms-front/src/api/price/set.js b/bms-front/src/api/price/set.js index db107037..dd7c7f89 100644 --- a/bms-front/src/api/price/set.js +++ b/bms-front/src/api/price/set.js @@ -1,440 +1,43 @@ -import request from '@/utils/request' +import { createPriceSetApi } from './createPriceSetApi' -export function listPriceSet(query) { - return request({ - url: '/price/set/list', - method: 'get', - params: query - }) -} +export const priceSetApi = createPriceSetApi('/price/set') -export function getPriceSet(priceSetId) { - return request({ - url: '/price/set/' + priceSetId, - method: 'get' - }) -} - -export function savePriceSet(data) { - return request({ - url: '/price/set', - method: 'post', - data: data - }) -} - -export function getPriceSetSaveTask(taskId) { - return request({ - url: '/price/set/save/task/' + taskId, - method: 'get', - timeout: 10 * 60 * 1000 - }) -} - -/** - * Submit save async and poll Redis progress until SUCCESS/FAIL. - * @returns {Promise<{ priceSetId: number|string, task: object }>} - */ -export async function savePriceSetAsync(data, onProgress) { - const submitRes = await savePriceSet(data) - const taskId = pickSaveTaskId(submitRes) - if (!taskId) { - throw new Error('missing save taskId') - } - const started = Date.now() - const maxMs = 30 * 60 * 1000 - while (Date.now() - started < maxMs) { - const res = await getPriceSetSaveTask(taskId) - const task = res.data || {} - if (typeof onProgress === 'function') { - onProgress(task) - } - if (task.status === 'SUCCESS') { - return { priceSetId: task.priceSetId, task } - } - if (task.status === 'FAIL') { - const err = new Error(task.error || task.message || 'save failed') - err.task = task - throw err - } - await new Promise(resolve => setTimeout(resolve, 400)) - } - throw new Error('save timeout') -} - -/** Accept data.taskId / data string / legacy msg UUID (Bms String overload). */ -function pickSaveTaskId(res) { - if (!res) return null - const d = res.data - if (d && typeof d === 'object' && d.taskId) return String(d.taskId) - if (typeof d === 'string' && d.trim()) return d.trim() - if (typeof res.msg === 'string') { - const m = res.msg.trim() - if (/^[a-f0-9]{32}$/i.test(m)) return m - if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(m)) { - return m.replace(/-/g, '') - } - } - return null -} - -export function updatePriceSet(data) { - return request({ - url: '/price/set', - method: 'put', - data: data - }) -} - -/** Update package name and note only. */ -export function updatePriceSetBasic(data) { - return request({ - url: '/price/set/basic', - method: 'put', - data: data - }) -} - -export function delPriceSet(priceSetIds) { - return request({ - url: '/price/set/' + priceSetIds, - method: 'delete' - }) -} - -export function submitPriceSet(priceSetId) { - return request({ - url: '/price/set/submit/' + priceSetId, - method: 'put' - }) -} - -export function approvePriceSet(priceSetId) { - return request({ - url: '/price/set/approve/' + priceSetId, - method: 'put' - }) -} - -export function rejectPriceSet(priceSetId) { - return request({ - url: '/price/set/reject/' + priceSetId, - method: 'put' - }) -} - -export function activatePriceSetVersion(psvId, data) { - return request({ - url: '/price/set/version/activate/' + psvId, - method: 'put', - data: data || {} - }) -} - -export function copyPriceSetVersion(psvId) { - return request({ - url: '/price/set/version/copy/' + psvId, - method: 'post' - }) -} - -/** Create empty draft version under package */ -export function createPriceSetVersion(priceSetId) { - return request({ - url: '/price/set/version/create/' + priceSetId, - method: 'post' - }) -} - -/** Import excel attachments for a price-set version (via prc_price_set_product.psv_id). */ -export function listPriceSetVersionExcelFiles(psvId) { - return request({ - url: '/price/set/version/' + psvId + '/excelFiles', - method: 'get' - }) -} - -export function deletePriceSetVersion(psvId) { - return request({ - url: '/price/set/version/' + psvId, - method: 'delete' - }) -} - -export function disablePriceSet(priceSetId) { - return request({ - url: '/price/set/disable/' + priceSetId, - method: 'put' - }) -} - -export function enablePriceSet(priceSetId) { - return request({ - url: '/price/set/enable/' + priceSetId, - method: 'put' - }) -} - -export function copyPriceSet(priceSetId) { - return request({ - url: '/price/set/copy/' + priceSetId, - method: 'post' - }) -} - -export function validatePriceSet(data) { - return request({ - url: '/price/set/validate', - method: 'post', - data: data - }) -} - -export function listSetDetail(query) { - return request({ - url: '/price/set/detail/list', - method: 'get', - params: query - }) -} - -/** All detail rows for one product under a version (no pagination). */ -export function listSetDetailByProduct(psvId, productCode) { - return request({ - url: '/price/set/detail/byProduct', - method: 'get', - params: { psvId, productCode } - }) -} - -/** All detail rows for a version (no pagination); one call instead of N byProduct. */ -export function listSetDetailByVersion(psvId) { - return request({ - url: '/price/set/detail/byVersion', - method: 'get', - params: { psvId } - }) -} - -/** 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 */ -export function listSetDetailOptions(psvId) { - return request({ - url: '/price/set/detail/options', - method: 'get', - params: { psvId } - }) -} - -export function addSetDetail(data) { - return request({ - url: '/price/set/detail', - method: 'post', - data: data - }) -} - - -/** - * Async import quote Excel into FIXED package detail prices; poll until done. - * @param {File|Blob} file - * @param {{ priceSetId: string|number, psvId?: string|number, updateOnly?: boolean, replaceByProductCountry?: boolean, countryCode?: string }} query - * @param {(task: object) => void} [onProgress] - * @returns {Promise<{ task: object }>} - */ -function appendUploadFiles(formData, file) { - const list = Array.isArray(file) ? file : [file] - for (const item of list) { - const raw = item && (item.raw || item) - if (raw) formData.append('files', raw) - } -} - -export async function importSetDetailPricesAsync(file, query, onProgress) { - const formData = new FormData() - appendUploadFiles(formData, file) - const params = new URLSearchParams() - if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId) - if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId) - if (query && query.updateOnly) params.set('updateOnly', 'true') - // Package Excel build-price only (product+country replace scope) - if (query && query.replaceByProductCountry) params.set('replaceByProductCountry', 'true') - if (query && query.countryCode) params.set('countryCode', query.countryCode) - const submitRes = await request({ - url: '/price/set/detail/importPrices?' + params.toString(), - method: 'post', - data: formData, - headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false } - }) - const taskId = pickSaveTaskId(submitRes) - if (!taskId) { - throw new Error('missing import taskId') - } - return pollPriceSetSaveTask(taskId, onProgress) -} - -/** - * Preview pull product library (no DB write). Returns { products, details, message }. - */ -export function previewPullFromLibrary() { - return request({ - url: '/price/set/detail/previewPullFromLibrary', - method: 'post' - }) -} - -/** - * Preview Excel import (no DB write). - * @param {File|Blob} file - * @param {{ updateOnly?: boolean, replaceByProductCountry?: boolean, countryCode?: string, details?: Array }} [opts] - */ -export function previewImportDetailPrices(file, opts = {}) { - const formData = new FormData() - appendUploadFiles(formData, file) - if (opts.updateOnly) formData.append('updateOnly', 'true') - if (opts.replaceByProductCountry) formData.append('replaceByProductCountry', 'true') - if (opts.countryCode) formData.append('countryCode', opts.countryCode) - // updateOnly / Excel build-price merge need current draft details - if ((opts.updateOnly || opts.replaceByProductCountry || opts.countryCode) && Array.isArray(opts.details)) { - formData.append('detailsJson', JSON.stringify(opts.details)) - } - return request({ - url: '/price/set/detail/previewImportPrices', - method: 'post', - data: formData, - // FormData must not go through repeat-submit JSON.stringify (collapses to "{}") - headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }, - timeout: 10 * 60 * 1000 - }) -} - -/** - * Async pull product library into package version details; poll until done. - * @param {{ priceSetId: string|number, psvId?: string|number, force?: boolean }} query - * @param {(task: object) => void} [onProgress] - */ -export async function pullSetDetailsFromLibraryAsync(query, onProgress) { - const params = new URLSearchParams() - if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId) - if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId) - params.set('force', query && query.force ? 'true' : 'false') - const submitRes = await request({ - url: '/price/set/detail/pullFromLibrary?' + params.toString(), - method: 'post' - }) - const taskId = pickSaveTaskId(submitRes) - if (!taskId) { - throw new Error('missing pull taskId') - } - return pollPriceSetSaveTask(taskId, onProgress) -} - -async function pollPriceSetSaveTask(taskId, onProgress) { - const started = Date.now() - const maxMs = 30 * 60 * 1000 - while (Date.now() - started < maxMs) { - const res = await getPriceSetSaveTask(taskId) - const task = res.data || {} - if (typeof onProgress === 'function') { - onProgress(task) - } - if (task.status === 'SUCCESS') { - return { task } - } - if (task.status === 'FAIL') { - const err = new Error(task.error || task.message || 'task failed') - err.task = task - throw err - } - await new Promise(resolve => setTimeout(resolve, 400)) - } - throw new Error('task timeout') -} - -export function updateSetDetail(data) { - return request({ - url: '/price/set/detail', - method: 'put', - data: data - }) -} - -/** - * Submit async batch write of detail surcharge snapshots and poll until done. - * @returns {Promise<{ task: object }>} - */ -export async function updateSetDetailSurchargeAsync(details, onProgress) { - const submitRes = await request({ - url: '/price/set/detail/surcharge/async', - method: 'post', - data: details || [] - }) - const taskId = pickSaveTaskId(submitRes) - if (!taskId) { - throw new Error('missing surcharge taskId') - } - const started = Date.now() - const maxMs = 30 * 60 * 1000 - while (Date.now() - started < maxMs) { - const res = await getPriceSetSaveTask(taskId) - const task = res.data || {} - if (typeof onProgress === 'function') { - onProgress(task) - } - if (task.status === 'SUCCESS') { - return { task } - } - if (task.status === 'FAIL') { - const err = new Error(task.error || task.message || 'surcharge save failed') - err.task = task - throw err - } - await new Promise(resolve => setTimeout(resolve, 400)) - } - throw new Error('surcharge save timeout') -} - -export function delSetDetail(psdIds) { - return request({ - url: '/price/set/detail/' + psdIds, - method: 'delete' - }) -} - -export function listSetCustomer(query) { - return request({ - url: '/price/set/customer/list', - method: 'get', - params: query - }) -} - -export function addSetCustomer(data) { - return request({ - url: '/price/set/customer', - method: 'post', - data: data - }) -} - -export function batchAddSetCustomer(data) { - return request({ - url: '/price/set/customer/batch', - method: 'post', - data: data - }) -} - -export function updateSetCustomer(data) { - return request({ - url: '/price/set/customer', - method: 'put', - data: data - }) -} - -export function delSetCustomer(ids) { - return request({ - url: '/price/set/customer/' + ids, - method: 'delete' - }) -} +export const { + listPriceSet, + getPriceSet, + savePriceSet, + getPriceSetSaveTask, + savePriceSetAsync, + updatePriceSet, + updatePriceSetBasic, + delPriceSet, + submitPriceSet, + approvePriceSet, + rejectPriceSet, + activatePriceSetVersion, + copyPriceSetVersion, + createPriceSetVersion, + listPriceSetVersionExcelFiles, + deletePriceSetVersion, + disablePriceSet, + enablePriceSet, + copyPriceSet, + validatePriceSet, + listSetDetail, + listSetDetailByProduct, + listSetDetailByVersion, + listSetDetailOptions, + addSetDetail, + importSetDetailPricesAsync, + previewPullFromLibrary, + previewImportDetailPrices, + pullSetDetailsFromLibraryAsync, + updateSetDetail, + updateSetDetailSurchargeAsync, + delSetDetail, + listSetCustomer, + addSetCustomer, + batchAddSetCustomer, + updateSetCustomer, + delSetCustomer +} = priceSetApi diff --git a/bms-front/src/views/price/exclusive/create.vue b/bms-front/src/views/price/exclusive/create.vue new file mode 100644 index 00000000..e5472d7c --- /dev/null +++ b/bms-front/src/views/price/exclusive/create.vue @@ -0,0 +1,565 @@ + + + + + diff --git a/bms-front/src/views/price/exclusive/detail.vue b/bms-front/src/views/price/exclusive/detail.vue new file mode 100644 index 00000000..ab62b16e --- /dev/null +++ b/bms-front/src/views/price/exclusive/detail.vue @@ -0,0 +1,538 @@ + + + + + + + diff --git a/bms-front/src/views/price/exclusive/exclusiveQuote.vue b/bms-front/src/views/price/exclusive/exclusiveQuote.vue new file mode 100644 index 00000000..f5b978d9 --- /dev/null +++ b/bms-front/src/views/price/exclusive/exclusiveQuote.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/bms-front/src/views/price/set/components/create/PriceEditPanel.vue b/bms-front/src/views/price/set/components/create/PriceEditPanel.vue index 9cd0f36b..7da7f6e5 100644 --- a/bms-front/src/views/price/set/components/create/PriceEditPanel.vue +++ b/bms-front/src/views/price/set/components/create/PriceEditPanel.vue @@ -44,7 +44,9 @@ import { ArrowLeft } from '@element-plus/icons-vue' import StepDetails from './StepDetails.vue' import { usePriceSetRefData } from '../../composables/usePriceSetRefData' import { isBuildModeLockedVersion } from '../../utils/priceSet' -import { getPriceSet, savePriceSetAsync } from '@/api/price/set' +import { usePriceSetModule } from '../../composables/usePriceSetModule' + +const { api } = usePriceSetModule() const props = defineProps({ priceSetId: { type: [Number, String], default: undefined }, @@ -82,7 +84,7 @@ function load() { } loading.value = true loaded.value = false - getPriceSet(id).then(res => { + api.getPriceSet(id).then(res => { const data = res.data || {} setType.value = data.setType || 'FIXED' priceSetName.value = data.priceSetName || '' @@ -137,7 +139,7 @@ function reloadSavedProducts() { const id = props.priceSetId if (!id) return loading.value = true - getPriceSet(id).then(res => { + api.getPriceSet(id).then(res => { const data = res.data || {} const versions = data.versions || [] const qPsv = props.psvId || version.psvId @@ -154,7 +156,7 @@ function reloadSavedProducts() { async function reloadProductsAfterImportModeSave() { const id = props.priceSetId if (!id) return - const res = await getPriceSet(id) + const res = await api.getPriceSet(id) const data = res.data || {} const versions = data.versions || [] const qPsv = props.psvId || version.psvId @@ -190,7 +192,7 @@ async function finish() { priceBuildMode: version.priceBuildMode } } - await savePriceSetAsync(payload) + await api.savePriceSetAsync(payload) savedPriceBuildMode.value = version.priceBuildMode await nextTick() if (version.priceBuildMode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) { diff --git a/bms-front/src/views/price/set/components/create/StepDetails.vue b/bms-front/src/views/price/set/components/create/StepDetails.vue index 897bfb71..462219f2 100644 --- a/bms-front/src/views/price/set/components/create/StepDetails.vue +++ b/bms-front/src/views/price/set/components/create/StepDetails.vue @@ -14,7 +14,7 @@ @click="handlePullLibrary" >拉取产品列表 @@ -48,6 +48,7 @@

创建价格方式

@@ -227,9 +228,14 @@ import { surchargeAppliesToRow, toDetailPayload } from '../../utils/priceSet' -import { getPriceSet, listSetDetailByProduct, listSetDetailByVersion, importSetDetailPricesAsync, pullSetDetailsFromLibraryAsync, previewPullFromLibrary, previewImportDetailPrices, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set' +import { usePriceSetModule } from '../../composables/usePriceSetModule' import { listRegionOptions } from '@/api/data/region' +const mod = usePriceSetModule() +const { api } = mod +const onlyImportFresh = computed(() => !!mod.onlyImportFresh) +const showCountryImport = computed(() => !mod.exclusiveQuote) + const props = defineProps({ details: { type: Array, required: true }, products: { type: Array, default: () => [] }, @@ -326,7 +332,18 @@ const importActionLabel = computed(() => isModifyImport.value ? '导入修改价格' : '导入 Excel 建价' ) const importDialogTitle = computed(() => importActionLabel.value) +const importFreshCardDesc = computed(() => onlyImportFresh.value + ? '仅支持大客折扣报价表(目的地/运费/操作费),导入后直接生成专属报价明细' + : '不延用以前的产品价格,不带出上一版,上传 Excel 直接生成套餐销售价' +) const importDesc = computed(() => { + if (onlyImportFresh.value) { + return [ + '\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868\uff08\u542b\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u4e0e\u5404\u56fd\u4ef7\u8868\uff09\u3002', + '\u8868\u5934\u4e3a\u76ee\u7684\u5730 / \u91cd\u91cf\u6bb5 / \u8fd0\u8d39(CNY/KG) / \u64cd\u4f5c\u8d39(CNY/PCS)\uff0c\u4e0e\u666e\u901a\u5957\u9910 Excel \u6a21\u677f\u4e0d\u540c\u3002', + '\u4ec5\u300cExcel \u65b0\u5efa\u4ef7\u300d\uff1a\u4e0d\u62c9\u53d6\u4ea7\u54c1\u5e93\u3001\u4e0d\u5ef6\u7528\u4e0a\u4e00\u7248\uff0c\u5bfc\u5165\u540e\u76f4\u63a5\u751f\u6210\u9500\u552e\u4ef7\u660e\u7ec6\u3002' + ] + } if (isModifyImport.value) { return [ '适用威狮B2C小包价表(含「产品目录」)。', @@ -377,7 +394,7 @@ async function resolvePsvId(force) { return resolvedPsvId.value } if (!props.priceSetId) return null - const res = await getPriceSet(props.priceSetId) + const res = await api.getPriceSet(props.priceSetId) const data = res.data || {} const versions = data.versions || [] const fromData = (data.version && data.version.psvId) ? data.version : null @@ -420,7 +437,7 @@ async function loadLatestPriorVersionDraft() { if (!props.priceSetId) return false pulling.value = true try { - const res = await getPriceSet(props.priceSetId) + const res = await api.getPriceSet(props.priceSetId) const data = res.data || {} const versions = data.versions || [] // Only exclude the draft being edited. Do NOT use resolvedPsvId — it may @@ -437,7 +454,7 @@ async function loadLatestPriorVersionDraft() { return row }) if (!products.length) return false - const detailRes = await listSetDetailByVersion(prior.psvId) + const detailRes = await api.listSetDetailByVersion(prior.psvId) applyLocalDraft(products, detailRes.data || []) return true } finally { @@ -455,7 +472,7 @@ async function loadProductDetails(productCode, force) { } const psvId = await resolvePsvId(false) if (!psvId) return [] - const res = await listSetDetailByProduct(psvId, productCode) + const res = await api.listSetDetailByProduct(psvId, productCode) const rows = (res.data || []).map(d => hydrateRow(d, props.setType)) detailByProduct[productCode] = rows // keep parent details array in sync for any legacy callers @@ -586,6 +603,18 @@ async function canOpenImport() { return true } + +watch( + () => [onlyImportFresh.value, props.priceBuildMode, props.buildModeReady], + () => { + if (!onlyImportFresh.value || !props.buildModeReady) return + if (props.priceBuildMode) return + if (buildModeLocked.value) return + emit('update:priceBuildMode', 'IMPORT_FRESH') + }, + { immediate: true } +) + async function openImport() { if (!(await canOpenImport())) return importRef.value && importRef.value.open() @@ -625,7 +654,7 @@ async function runImportAsync(uploadFile, countryCode) { } const replaceByProductCountry = !isModifyImport.value || !!countryCode if (!props.livePersist) { - const res = await previewImportDetailPrices(files, { + const res = await api.previewImportDetailPrices(files, { updateOnly: isModifyImport.value && !countryCode, replaceByProductCountry, countryCode: countryCode || undefined, @@ -640,7 +669,7 @@ async function runImportAsync(uploadFile, countryCode) { applyLocalDraft(products, data.details || []) return data.message || '导入完成' } - const { task } = await importSetDetailPricesAsync( + const { task } = await api.importSetDetailPricesAsync( files, { priceSetId: props.priceSetId, @@ -693,7 +722,7 @@ async function reloadDetailsFromServer() { if (!props.priceSetId) return try { clearDetailCache() - const res = await getPriceSet(props.priceSetId) + const res = await api.getPriceSet(props.priceSetId) const data = res.data || {} const versions = data.versions || [] const qPsv = resolvedPsvId.value || props.psvId @@ -754,7 +783,7 @@ async function pullAllFromLibrary(opts = {}) { if (!props.livePersist) { importProgressMessage.value = '正在拉取产品库…' importProgressPercent.value = 40 - const res = await previewPullFromLibrary() + const res = await api.previewPullFromLibrary() const data = res.data || {} applyLocalDraft(data.products || [], data.details || []) importProgressPercent.value = 100 @@ -764,7 +793,7 @@ async function pullAllFromLibrary(opts = {}) { } return { added: (props.products || []).length } } - const { task } = await pullSetDetailsFromLibraryAsync( + const { task } = await api.pullSetDetailsFromLibraryAsync( { priceSetId: props.priceSetId, psvId: await resolvePsvId(false), force }, applyBuildProgress ) @@ -864,7 +893,7 @@ async function persistDetail(row, withSurcharge) { const payload = toDetailPayload(row) payload.priceSetId = props.priceSetId if (!withSurcharge) delete payload.surcharges - await updateSetDetail(payload) + await api.updateSetDetail(payload) } /** drawer hover remove surcharge -> write snapshot for this row */ @@ -1035,7 +1064,7 @@ function onSurchargeConfirm(picked) { return } proxy.$modal.msgSuccess(okText + ',正在后台写入…') - updateSetDetailSurchargeAsync(payload).then(() => { + api.updateSetDetailSurchargeAsync(payload).then(() => { proxy.$modal.msgSuccess(okText + ',已写入数据库') }).catch(e => { proxy.$modal.msgError((e && e.message) ? e.message : '附加费后台写入失败') @@ -1048,7 +1077,7 @@ async function removeProduct(code) { const rows = await loadProductDetails(code) const ids = rows.filter(d => d && d.psdId).map(d => d.psdId) if (ids.length) { - await delSetDetail(ids.join(',')) + await api.delSetDetail(ids.join(',')) } } catch (e) { return diff --git a/bms-front/src/views/price/set/components/detail/SetCustomerDialog.vue b/bms-front/src/views/price/set/components/detail/SetCustomerDialog.vue index 0d278a9a..3fdc5ed2 100644 --- a/bms-front/src/views/price/set/components/detail/SetCustomerDialog.vue +++ b/bms-front/src/views/price/set/components/detail/SetCustomerDialog.vue @@ -83,7 +83,9 @@ * 套餐绑定客户 · 新增(支持批量) / 编辑弹窗 */ import { createEmptyCustomerRow } from '../../utils/priceSet' -import { batchAddSetCustomer, updateSetCustomer } from '@/api/price/set' +import { usePriceSetModule } from '../../composables/usePriceSetModule' + +const { api } = usePriceSetModule() const props = defineProps({ priceSetId: { @@ -174,7 +176,7 @@ async function submit() { try { if (isEdit.value) { const payload = { ...form.value, priceSetId: props.priceSetId } - await updateSetCustomer(payload) + await api.updateSetCustomer(payload) proxy.$modal.msgSuccess('保存成功') } else { const customers = selectedCustomers.value.map(c => ({ @@ -182,7 +184,7 @@ async function submit() { customerCode: c.customerCode, customerName: c.customerName })) - const res = await batchAddSetCustomer({ + const res = await api.batchAddSetCustomer({ priceSetId: props.priceSetId, bindPriority: form.value.bindPriority, prBegindate: form.value.prBegindate, diff --git a/bms-front/src/views/price/set/components/detail/SetCustomerTable.vue b/bms-front/src/views/price/set/components/detail/SetCustomerTable.vue index ce57a7d5..7c955ef2 100644 --- a/bms-front/src/views/price/set/components/detail/SetCustomerTable.vue +++ b/bms-front/src/views/price/set/components/detail/SetCustomerTable.vue @@ -12,7 +12,7 @@ size="small" icon="Plus" @click="dialogRef.open()" - v-hasPermi="['price:set:edit']" + v-hasPermi="editPerm" >添加客户 @@ -31,8 +31,8 @@ @@ -55,7 +55,10 @@ import SetSectionCard from './SetSectionCard.vue' import SetCustomerDialog from './SetCustomerDialog.vue' import { isChildEditable } from '../../utils/priceSet' -import { delSetCustomer } from '@/api/price/set' +import { usePriceSetModule, usePriceSetPerm } from '../../composables/usePriceSetModule' + +const { api } = usePriceSetModule() +const editPerm = usePriceSetPerm('edit') const props = defineProps({ /** 套餐详情对象 */ @@ -75,7 +78,7 @@ const customers = computed(() => props.set.customers || []) const editable = computed(() => isChildEditable(props.set.status)) function remove(row) { - proxy.$modal.confirm('确认解绑该客户?').then(() => delSetCustomer(row.customerPricesetId)).then(() => { + proxy.$modal.confirm('确认解绑该客户?').then(() => api.delSetCustomer(row.customerPricesetId)).then(() => { proxy.$modal.msgSuccess('已解绑') emit('refresh') }).catch(() => {}) diff --git a/bms-front/src/views/price/set/components/detail/SetDetailDialog.vue b/bms-front/src/views/price/set/components/detail/SetDetailDialog.vue index 34190bb6..4eaa06ac 100644 --- a/bms-front/src/views/price/set/components/detail/SetDetailDialog.vue +++ b/bms-front/src/views/price/set/components/detail/SetDetailDialog.vue @@ -164,7 +164,9 @@ import SurchargeTags from '../SurchargeTags.vue' import { useDetailCascade } from '../../composables/usePriceSetRefData' import { createEmptyDetailRow, isFixedType, toEditableDetailRow } from '../../utils/priceSet' -import { addSetDetail, updateSetDetail } from '@/api/price/set' +import { usePriceSetModule } from '../../composables/usePriceSetModule' + +const { api } = usePriceSetModule() const props = defineProps({ /** 所属套餐ID */ @@ -228,7 +230,7 @@ async function submit() { const payload = { ...form.value, priceSetId: props.priceSetId } delete payload._countryKey submitting.value = true - const req = payload.psdId ? updateSetDetail(payload) : addSetDetail(payload) + const req = payload.psdId ? api.updateSetDetail(payload) : api.addSetDetail(payload) req.then(() => { proxy.$modal.msgSuccess('保存成功') visible.value = false diff --git a/bms-front/src/views/price/set/components/detail/SetDetailHeader.vue b/bms-front/src/views/price/set/components/detail/SetDetailHeader.vue index 3aa2cd58..c3b76347 100644 --- a/bms-front/src/views/price/set/components/detail/SetDetailHeader.vue +++ b/bms-front/src/views/price/set/components/detail/SetDetailHeader.vue @@ -2,7 +2,7 @@
-

{{ set.priceSetName || '套餐详情' }}

+

{{ set.priceSetName || mod.labels.detailTitleFallback }}

{{ set.priceSetCode }} {{ setStatusLabel(set) }}
@@ -20,14 +20,14 @@ type="danger" plain @click="emit('disable')" - v-hasPermi="['price:set:audit']" + v-hasPermi="auditPerm" >停用 启用
@@ -35,6 +35,10 @@