diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/LabelPackageController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/LabelPackageController.java new file mode 100644 index 00000000..2b3f4fad --- /dev/null +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/LabelPackageController.java @@ -0,0 +1,303 @@ +package com.bms.web.controller.price; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.bms.common.annotation.Log; +import com.bms.common.core.controller.BaseController; +import com.bms.common.core.domain.AjaxResult; +import com.bms.common.core.page.TableDataInfo; +import com.bms.common.enums.BusinessType; +import com.bms.common.exception.ServiceException; +import com.bms.price.domain.PaySaveCommand; +import com.bms.price.domain.PrcCustomerPriceSet; +import com.bms.price.domain.PrcCustomerPriceSetBatch; +import com.bms.price.domain.PrcPriceSet; +import com.bms.price.service.IPayProductService; +import com.bms.price.service.IPrcPriceSetService; + +/** + * 打单套餐:套餐壳 + 应收价卡(pay_*)。 + */ +@RestController +@RequestMapping("/price/label") +public class LabelPackageController extends BaseController +{ + @Autowired + private IPrcPriceSetService priceSetService; + + @Autowired + private IPayProductService payProductService; + + @PreAuthorize("@ss.hasPermi('price:label:list')") + @GetMapping("/list") + public TableDataInfo list(PrcPriceSet set) + { + set.setSetCategory(PrcPriceSet.CATEGORY_LABEL); + startPage(); + List list = priceSetService.selectPrcPriceSetList(set); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('price:label:query')") + @GetMapping("/{priceSetId:[0-9]+}") + public AjaxResult getInfo(@PathVariable Long priceSetId) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + return success(priceSetService.selectPrcPriceSetById(priceSetId)); + } + + @PreAuthorize("@ss.hasPermi('price:label:edit')") + @Log(title = "打单套餐基本信息", businessType = BusinessType.UPDATE) + @PutMapping("/basic") + public AjaxResult editBasic(@RequestBody PrcPriceSet set) + { + priceSetService.assertSetCategory(set.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL); + set.setUpdateBy(getUsername()); + return toAjax(priceSetService.updateBasicInfo(set)); + } + + @PreAuthorize("@ss.hasPermi('price:label:audit')") + @Log(title = "打单套餐停用", businessType = BusinessType.UPDATE) + @PutMapping("/{priceSetId}/disable") + public AjaxResult disable(@PathVariable Long priceSetId) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + return toAjax(priceSetService.disable(priceSetId, getUsername())); + } + + @PreAuthorize("@ss.hasPermi('price:label:audit')") + @Log(title = "打单套餐启用", businessType = BusinessType.UPDATE) + @PutMapping("/{priceSetId}/enable") + public AjaxResult enable(@PathVariable Long priceSetId) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + return toAjax(priceSetService.enable(priceSetId, getUsername())); + } + + @PreAuthorize("@ss.hasPermi('price:label:remove')") + @Log(title = "打单套餐删除", businessType = BusinessType.DELETE) + @DeleteMapping("/{priceSetIds}") + public AjaxResult remove(@PathVariable Long[] priceSetIds) + { + if (priceSetIds != null) + { + for (Long id : priceSetIds) + { + priceSetService.assertSetCategory(id, PrcPriceSet.CATEGORY_LABEL); + } + } + return toAjax(priceSetService.deletePrcPriceSetByIds(priceSetIds)); + } + + @PreAuthorize("@ss.hasPermi('price:label:import')") + @PostMapping("/preview") + public AjaxResult preview(@RequestParam("file") MultipartFile file, + @RequestParam(value = "dictValue", required = false) String dictValue, + @RequestParam(value = "progressId", required = false) String progressId, + @RequestParam(value = "priceSetId", required = false) Long priceSetId) throws Exception + { + if (file == null || file.isEmpty()) + { + throw new ServiceException("请上传价卡文件"); + } + String name = file.getOriginalFilename(); + if (name == null || !name.toLowerCase(Locale.ROOT).endsWith(".xlsx")) + { + throw new ServiceException("请上传xlsx价卡"); + } + if (priceSetId != null) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + } + // 打单独立预览:已存在按本套餐判断,不与应付公共模板互挡 + return success(payProductService.previewLabel(file.getBytes(), name, dictValue, progressId, priceSetId)); + } + + @PreAuthorize("@ss.hasPermi('price:label:import')") + @GetMapping("/preview/progress") + public AjaxResult parseProgress(@RequestParam("progressId") String progressId) + { + return success(payProductService.parseProgress(progressId)); + } + + @PreAuthorize("@ss.hasPermi('price:label:import')") + @Log(title = "打单套餐导入", businessType = BusinessType.IMPORT) + @PostMapping("/importSave") + public AjaxResult importSave(@RequestBody PaySaveCommand command) + { + if (command == null) + { + throw new ServiceException("保存参数不能为空"); + } + String message = payProductService.saveLabel(command); + Map payload = new HashMap(); + payload.put("message", message); + payload.put("priceSetId", command.getPriceSetId()); + return success(payload); + } + + @PreAuthorize("@ss.hasPermi('price:label:list')") + @GetMapping("/{priceSetId}/versions") + public AjaxResult versions(@PathVariable Long priceSetId) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + return success(payProductService.selectLabelVersions(priceSetId)); + } + + @PreAuthorize("@ss.hasPermi('price:label:edit')") + @Log(title = "打单套餐启动版本", businessType = BusinessType.UPDATE) + @PutMapping("/{priceSetId}/versions/{versionNo}/activate") + public AjaxResult activateVersion(@PathVariable Long priceSetId, @PathVariable Integer versionNo, + @RequestBody Map body) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + Date effectiveTime = parseEffectiveTime(body == null ? null : body.get("effectiveTime")); + String message = payProductService.activateLabelVersion(priceSetId, versionNo, effectiveTime); + Map payload = new HashMap(); + payload.put("message", message); + return success(payload); + } + + @PreAuthorize("@ss.hasPermi('price:label:list')") + @GetMapping("/{priceSetId}/products") + public AjaxResult products(@PathVariable Long priceSetId, + @RequestParam(value = "versionNo", required = false) Integer versionNo) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + if (versionNo != null) + { + return success(payProductService.selectLabelProductsByVersion(priceSetId, versionNo)); + } + return success(payProductService.selectLabelProducts(priceSetId)); + } + + @PreAuthorize("@ss.hasPermi('price:label:query')") + @GetMapping("/product/{productId}") + public AjaxResult productDetail(@PathVariable Long productId, + @RequestParam(value = "termId", required = false) Long termId) + { + if (termId != null) + { + return success(payProductService.selectPayProductDetailByTerm(termId)); + } + return success(payProductService.selectPayProductDetail(productId)); + } + + @PreAuthorize("@ss.hasPermi('price:label:remove')") + @Log(title = "打单套餐移除产品", businessType = BusinessType.DELETE) + @DeleteMapping("/{priceSetId}/products/{productIds}") + public AjaxResult removeProducts(@PathVariable Long priceSetId, @PathVariable Long[] productIds) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + return toAjax(payProductService.removeLabelProducts(priceSetId, productIds)); + } + + @PreAuthorize("@ss.hasPermi('price:label:list')") + @GetMapping("/customer/list") + public TableDataInfo customerList(PrcCustomerPriceSet customer) + { + if (customer != null && customer.getPriceSetId() != null) + { + priceSetService.assertSetCategory(customer.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL); + } + startPage(); + List list = priceSetService.selectCustomerList(customer); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('price:label:edit')") + @Log(title = "打单套餐客户绑定", businessType = BusinessType.INSERT) + @PostMapping("/customer") + public AjaxResult addCustomer(@RequestBody PrcCustomerPriceSet customer) + { + if (customer == null || customer.getPriceSetId() == null) + { + throw new ServiceException("请选择打单套餐"); + } + priceSetService.assertSetCategory(customer.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL); + customer.setCreateBy(getUsername()); + return toAjax(priceSetService.insertCustomer(customer)); + } + + @PreAuthorize("@ss.hasPermi('price:label:edit')") + @Log(title = "打单套餐客户批量绑定", businessType = BusinessType.INSERT) + @PostMapping("/customer/batch") + public AjaxResult batchAddCustomer(@RequestBody PrcCustomerPriceSetBatch batch) + { + if (batch == null || batch.getPriceSetId() == null) + { + throw new ServiceException("请选择打单套餐"); + } + priceSetService.assertSetCategory(batch.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL); + batch.setCreateBy(getUsername()); + batch.setUpdateBy(getUsername()); + return success(priceSetService.batchInsertCustomers(batch)); + } + + @PreAuthorize("@ss.hasPermi('price:label:edit')") + @Log(title = "打单套餐客户绑定", businessType = BusinessType.UPDATE) + @PutMapping("/customer") + public AjaxResult editCustomer(@RequestBody PrcCustomerPriceSet customer) + { + if (customer == null) + { + throw new ServiceException("客户绑定参数不能为空"); + } + if (customer.getPriceSetId() != null) + { + priceSetService.assertSetCategory(customer.getPriceSetId(), PrcPriceSet.CATEGORY_LABEL); + } + customer.setUpdateBy(getUsername()); + return toAjax(priceSetService.updateCustomer(customer)); + } + + @PreAuthorize("@ss.hasPermi('price:label:edit')") + @Log(title = "打单套餐客户绑定", businessType = BusinessType.DELETE) + @DeleteMapping("/customer/{customerPricesetIds}") + public AjaxResult removeCustomer(@PathVariable Long[] customerPricesetIds) + { + return toAjax(priceSetService.deleteCustomerByIds(customerPricesetIds)); + } + + private Date parseEffectiveTime(Object raw) + { + if (raw == null) + { + return null; + } + if (raw instanceof Date) + { + return (Date) raw; + } + String text = String.valueOf(raw).trim(); + if (text.isEmpty() || "null".equalsIgnoreCase(text)) + { + return null; + } + // 兼容 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss + if (text.length() >= 19) + { + return com.bms.common.utils.DateUtils.parseDate(text); + } + if (text.length() >= 10) + { + return com.bms.common.utils.DateUtils.parseDate(text.substring(0, 10)); + } + return com.bms.common.utils.DateUtils.parseDate(text); + } +} diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java index 086e73ec..df111c1b 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java @@ -135,7 +135,7 @@ public class PayProductController extends BaseController payProductService.unbindChannel(channelId, payType); return success(); } - payProductService.bindChannel(channelId, link.getProductId(), link.getPriceSetId(), payType); + payProductService.bindChannel(channelId, link.getProductId(), payType); return success(payProductService.selectChannelPrice(channelId, payType)); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/ChannelProductMap.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/ChannelProductMap.java index 07e9f724..f3df0c3a 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/ChannelProductMap.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/ChannelProductMap.java @@ -20,9 +20,6 @@ public class ChannelProductMap private Long productId; - /** 应收套餐 prc_price_set.price_set_id */ - private Long priceSetId; - /** AR 应收,AP 应付 */ private String payType; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/LabelPackageVersion.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/LabelPackageVersion.java new file mode 100644 index 00000000..6f745750 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/LabelPackageVersion.java @@ -0,0 +1,39 @@ +package com.bms.price.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** + * 打单套餐价卡版本(按 pay_product_term.version_no 聚合,不走价格套餐 prc_price_set_version)。 + */ +@Data +public class LabelPackageVersion +{ + private Long priceSetId; + + private Integer versionNo; + + /** 展示用,如 V1 */ + private String versionLabel; + + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date effectiveTime; + + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date expireTime; + + /** 与 pay_product_term.status 一致:0待生效 1生效 2已失效 */ + private String status; + + private Integer productCount; + + /** status=1 生效条数 */ + private Integer activeCount; + + /** status=0 待生效条数 */ + private Integer pendingCount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayExtraLine.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayExtraLine.java index 0bace8b0..16453a2c 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayExtraLine.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayExtraLine.java @@ -12,6 +12,9 @@ public class PayExtraLine private Long productId; + /** 价卡周期,对应 pay_product_term.term_id;与 productId 组成明细维度 */ + private Long termId; + /** 邮编 或 配置 */ private String lineType; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProductTerm.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProductTerm.java index 9ce58f39..ce5bdb2f 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProductTerm.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProductTerm.java @@ -22,6 +22,9 @@ public class PayProductTerm extends BaseEntity private Long productId; + /** 打单套餐ID;应付公共模板为空 */ + private Long priceSetId; + private Integer versionNo; @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") @@ -30,7 +33,7 @@ public class PayProductTerm extends BaseEntity @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private Date effectiveEnd; - /** 0 生效,1 失效 */ + /** 0待生效 1生效 2已失效 */ private String status; private String productName; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySaveCommand.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySaveCommand.java index 53d6b3da..702c33fe 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySaveCommand.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySaveCommand.java @@ -13,5 +13,17 @@ public class PaySaveCommand /** 为 true 时,同文件同表同名的旧产品先删后插 */ private boolean overwrite; + /** 打单套餐ID;有值时走 LABEL 导入(稳定 product_id + term.price_set_id) */ + private Long priceSetId; + + /** 打单套餐名称;priceSetId 为空时新建套餐壳 */ + private String priceSetName; + + /** 打单套餐备注 */ + private String priceSetNote; + + /** 编辑待生效版本时传入,覆盖该 version_no,不新建版本号 */ + private Integer versionNo; + private List products = new ArrayList<>(); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurcharge.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurcharge.java index 4856e32b..a3b20f7d 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurcharge.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurcharge.java @@ -15,7 +15,10 @@ public class PaySurcharge private Long productId; - /** 所属价卡版本,跟产品版本一起变 */ + /** 价卡周期,对应 pay_product_term.term_id;与 productId 组成明细维度 */ + private Long termId; + + /** 所属价卡版本,跟产品版本一起变(展示兼容) */ private Integer versionNo; private String sectionName; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayWeightBand.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayWeightBand.java index fa06ae74..3ff0dc7b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayWeightBand.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayWeightBand.java @@ -13,7 +13,10 @@ public class PayWeightBand private Long productId; - /** 所属价卡版本,跟产品版本一起变 */ + /** 价卡周期,对应 pay_product_term.term_id;与 productId 组成明细维度 */ + private Long termId; + + /** 所属价卡版本,跟产品版本一起变(展示兼容) */ private Integer versionNo; /** 报价区块,例如派送费、偏远附加费 */ 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 5bcc1102..a1bee1ae 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 @@ -19,6 +19,8 @@ public class PrcPriceSet extends BaseEntity public static final String CATEGORY_PACKAGE = "PACKAGE"; /** Boss exclusive quote. */ public static final String CATEGORY_EXCLUSIVE = "EXCLUSIVE"; + /** Label / 打单套餐(应收价卡套餐). */ + public static final String CATEGORY_LABEL = "LABEL"; private Long priceSetId; private String priceSetCode; @@ -26,7 +28,7 @@ public class PrcPriceSet extends BaseEntity private String priceSetNote; /** FIXED / DISCOUNT */ private String setType; - /** PACKAGE=ordinary set / EXCLUSIVE=boss exclusive quote */ + /** PACKAGE=ordinary set / EXCLUSIVE=boss exclusive quote / LABEL=打单套餐 */ private String setCategory; private BigDecimal defaultDiscount; private String defaultType; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductMapper.java index dca13d49..07a7f1fa 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductMapper.java @@ -38,16 +38,33 @@ public interface PayProductMapper List selectWeightBands(@Param("productIds") List productIds); + List selectWeightBandsByTerm(@Param("termIds") List termIds); + List selectSurcharges(@Param("productIds") List productIds); + List selectSurchargesByTerm(@Param("termIds") List termIds); + List selectExtraLines(@Param("productIds") List productIds); + List selectExtraLinesByTerm(@Param("termIds") List termIds); + int deleteWeightBands(@Param("productIds") List productIds); int deleteSurcharges(@Param("productIds") List productIds); int deleteExtraLines(@Param("productIds") List productIds); + int deleteWeightBandsByTerm(@Param("termIds") List termIds); + + int deleteSurchargesByTerm(@Param("termIds") List termIds); + + int deleteExtraLinesByTerm(@Param("termIds") List termIds); + + Long selectTemplateIdByType(@Param("dictValue") String dictValue, @Param("productType") String productType); + + /** 打单专用:命中仅属于打单套餐的 AR 产品,不命中应付公共模板 */ + Long selectLabelProductIdByDict(@Param("dictValue") String dictValue); + int deletePayProducts(@Param("productIds") List productIds); List selectFamilyIds(@Param("rootId") Long rootId); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductTermMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductTermMapper.java index 3d7cc69c..ff947b1c 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductTermMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductTermMapper.java @@ -3,6 +3,7 @@ package com.bms.price.mapper; import java.util.Date; import java.util.List; import org.apache.ibatis.annotations.Param; +import com.bms.price.domain.LabelPackageVersion; import com.bms.price.domain.PayProductTerm; /** @@ -16,6 +17,24 @@ public interface PayProductTermMapper PayProductTerm selectByProductId(Long productId); + List selectByPriceSetId(@Param("priceSetId") Long priceSetId); + + List selectByPriceSetIdAndVersion(@Param("priceSetId") Long priceSetId, + @Param("versionNo") Integer versionNo); + + /** 打单套餐:按 version_no 聚合版本(含已失效) */ + List selectLabelVersionGroups(@Param("priceSetId") Long priceSetId); + + /** 打单套餐:批量改某一版的生效起止与状态 */ + int updateLabelVersionPeriod(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo, + @Param("effectiveStart") Date effectiveStart, @Param("effectiveEnd") Date effectiveEnd, + @Param("status") String status, @Param("updateBy") String updateBy); + + Integer selectMaxVersionByProduct(@Param("productId") Long productId, @Param("priceSetId") Long priceSetId); + + /** 打单套餐下最大 version_no */ + Integer selectMaxVersionByPriceSet(@Param("priceSetId") Long priceSetId); + int insertTerm(PayProductTerm term); int updatePeriod(PayProductTerm term); @@ -25,7 +44,29 @@ public interface PayProductTermMapper /** 新版本生效时,把同一产品仍生效的旧版本关掉,结束日收到新版本开始的前一天 */ int closeOpenTerms(@Param("dictValue") String dictValue, @Param("endDate") Date endDate, @Param("updateBy") String updateBy); + /** 打单套餐:按套餐+产品关闭仍生效的旧周期 */ + int closeOpenTermsBySetProduct(@Param("priceSetId") Long priceSetId, @Param("productId") Long productId, + @Param("endDate") Date endDate, @Param("updateBy") String updateBy); + + /** 打单:只关掉某产品本套餐待生效 term */ + int closePendingBySetProduct(@Param("priceSetId") Long priceSetId, @Param("productId") Long productId, + @Param("endDate") Date endDate, @Param("updateBy") String updateBy); + + /** 打单:启动某版本时,关掉本套餐其它版本的生效 term */ + int closeEffectExceptVersion(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo, + @Param("endDate") Date endDate, @Param("updateBy") String updateBy); + + /** 打单:预约未来生效时,只写旧版失效日,仍保持生效;并恢复被提前关掉但未到期的旧版 */ + int scheduleEndExceptVersion(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo, + @Param("endDate") Date endDate, @Param("today") Date today, @Param("updateBy") String updateBy); + + /** 打单:恢复「失效日未到却被标成已失效」的 term */ + int restorePrematureClosed(@Param("priceSetId") Long priceSetId, @Param("today") Date today, + @Param("updateBy") String updateBy); + int deleteByProductIds(@Param("productIds") List productIds); + int deleteByTermIds(@Param("termIds") List termIds); + int updateProductEffectiveDate(@Param("productId") Long productId, @Param("effectiveDate") Date effectiveDate); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java index 5ce504bb..ef6a93aa 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java @@ -3,6 +3,7 @@ package com.bms.price.service; import java.io.OutputStream; import java.util.List; import java.util.Date; +import com.bms.price.domain.LabelPackageVersion; import com.bms.price.domain.PayParseProgress; import com.bms.price.domain.PayPreview; import com.bms.price.domain.PayPriceCompare; @@ -54,7 +55,34 @@ public interface IPayProductService PayProduct selectChannelPrice(String channelId, String payType); /** 只绑定产品编号,不产生新版本;同时落 channel_code。payType 为空时按应付 AP */ - void bindChannel(String channelId, Long productId, Long priceSetId, String payType); + void bindChannel(String channelId, Long productId, String payType); + + /** + * 打单套餐预览:解析复用应付解析器,但「已存在」只按本套餐 price_set_id 判断, + * 不与应付公共模板互相挡导入。 + */ + PayPreview previewLabel(byte[] bytes, String filename, String dictValue, String progressId, Long priceSetId); + + /** 打单套餐导入:稳定 product_id,明细挂 term_id,term 写 price_set_id;跳过仅限本套餐已有生效版 */ + String saveLabel(PaySaveCommand command); + + /** 打单套餐价卡版本列表(按 term.version_no 聚合) */ + List selectLabelVersions(Long priceSetId); + + /** 启动/修改打单套餐某一版生效时间,并视情况启用套餐 */ + String activateLabelVersion(Long priceSetId, Integer versionNo, Date effectiveTime); + + /** 打单套餐内当前生效/待生效产品列表 */ + List selectLabelProducts(Long priceSetId); + + /** 打单套餐某一版本的产品列表 */ + List selectLabelProductsByVersion(Long priceSetId, Integer versionNo); + + /** 按 term 加载明细 */ + PayProduct selectPayProductDetailByTerm(Long termId); + + /** 从打单套餐移除产品(失效/清理对应 term 明细) */ + int removeLabelProducts(Long priceSetId, Long[] productIds); /** 取消渠道上指定收付类型的产品。payType 为空时按应付 AP */ void unbindChannel(String channelId, String payType); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java index 209ba71e..d871ca94 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java @@ -25,6 +25,7 @@ import com.bms.common.utils.SecurityUtils; import com.bms.common.utils.file.OssStorage; import com.bms.common.utils.uuid.IdUtils; import com.bms.price.domain.ChannelProductMap; +import com.bms.price.domain.LabelPackageVersion; import com.bms.price.domain.PayExtraLine; import com.bms.price.domain.PayParseProgress; import com.bms.price.domain.PayPreview; @@ -35,11 +36,13 @@ import com.bms.price.domain.PayProductTerm; import com.bms.price.domain.PaySaveCommand; import com.bms.price.domain.PaySurcharge; import com.bms.price.domain.PayWeightBand; +import com.bms.price.domain.PrcPriceSet; import com.bms.masterdata.domain.ServiceChannel; import com.bms.masterdata.mapper.ServiceChannelMapper; import com.bms.price.mapper.ChannelProductMapMapper; import com.bms.price.mapper.PayProductMapper; import com.bms.price.mapper.PayProductTermMapper; +import com.bms.price.mapper.PrcPriceSetMapper; import com.bms.price.pay.PayParseProgressListener; import com.bms.price.pay.PayWorkbookParser; import com.bms.price.service.IPayProductService; @@ -59,6 +62,13 @@ public class PayProductServiceImpl implements IPayProductService private static final String PAY_TYPE_AR = "AR"; + /** pay_product_term.status:0待生效 1生效 2已失效 */ + private static final String TERM_PENDING = "0"; + + private static final String TERM_EFFECT = "1"; + + private static final String TERM_EXPIRED = "2"; + @Autowired private PayProductMapper payProductMapper; @@ -71,6 +81,9 @@ public class PayProductServiceImpl implements IPayProductService @Autowired private PayProductTermMapper payProductTermMapper; + @Autowired + private PrcPriceSetMapper priceSetMapper; + @Autowired private ISysDictTypeService dictTypeService; @@ -97,6 +110,21 @@ public class PayProductServiceImpl implements IPayProductService @Override public PayPreview preview(byte[] bytes, String filename, String dictValue, String progressId) + { + return doPreview(bytes, filename, dictValue, progressId, false, null); + } + + @Override + public PayPreview previewLabel(byte[] bytes, String filename, String dictValue, String progressId, Long priceSetId) + { + return doPreview(bytes, filename, dictValue, progressId, true, priceSetId); + } + + /** + * @param labelMode true=打单套餐:已存在仅看本套餐;false=应付:看公共模板 + */ + private PayPreview doPreview(byte[] bytes, String filename, String dictValue, String progressId, + boolean labelMode, Long priceSetId) { String progressKey = progressKey(progressId); PayParseProgressListener listener = progressKey == null ? null : (percent, message) -> @@ -115,9 +143,24 @@ public class PayProductServiceImpl implements IPayProductService for (PayProduct product : preview.getProducts()) { product.setFilePath(stored); - Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue()); - product.setDuplicated(existsId != null); - product.setExistsId(existsId); + if (labelMode) + { + product.setProductType(PAY_TYPE_AR); + // 只认「打单侧」产品,绝不把应付公共模板标成已存在 + Long existsId = isBlank(product.getDictValue()) ? null + : payProductMapper.selectLabelProductIdByDict(product.getDictValue()); + product.setExistsId(existsId); + // 仅待生效占用标「已存在」;已生效可继续建新版本 + boolean pendingInSet = priceSetId != null && existsId != null + && hasLabelTermStatus(priceSetId, existsId, TERM_PENDING); + product.setDuplicated(pendingInSet); + } + else + { + Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue()); + product.setDuplicated(existsId != null); + product.setExistsId(existsId); + } } writeProgress(progressKey, 100, "解析完成", PayParseProgress.SUCCESS); return preview; @@ -220,7 +263,6 @@ public class PayProductServiceImpl implements IPayProductService product.setFilePath(resolveSource(product)); prepareCounts(product); fitColumns(product); - product.setProductId(null); product.setChannelId(null); product.setCreateBy(operator); product.setCreateTime(new Date()); @@ -231,13 +273,14 @@ public class PayProductServiceImpl implements IPayProductService } else { + product.setProductId(null); product.setOriginId(null); product.setVersionNo(1); payProductMapper.insertPayProduct(product); + openTerm(product, false, null); insertBands(product); insertSurcharges(product); insertExtras(product); - openTerm(product, false); } saved++; } @@ -426,11 +469,29 @@ public class PayProductServiceImpl implements IPayProductService private void replaceChildren(Long productId, PayProduct product) { - List ids = Collections.singletonList(productId); - payProductMapper.deleteWeightBands(ids); - payProductMapper.deleteSurcharges(ids); - payProductMapper.deleteExtraLines(ids); product.setProductId(productId); + if (product.getTermId() == null) + { + PayProductTerm term = payProductTermMapper.selectByProductId(productId); + if (term != null) + { + product.setTermId(term.getTermId()); + } + } + if (product.getTermId() != null) + { + List termIds = Collections.singletonList(product.getTermId()); + payProductMapper.deleteWeightBandsByTerm(termIds); + payProductMapper.deleteSurchargesByTerm(termIds); + payProductMapper.deleteExtraLinesByTerm(termIds); + } + else + { + List ids = Collections.singletonList(productId); + payProductMapper.deleteWeightBands(ids); + payProductMapper.deleteSurcharges(ids); + payProductMapper.deleteExtraLines(ids); + } insertBands(product); insertSurcharges(product); insertExtras(product); @@ -511,7 +572,7 @@ public class PayProductServiceImpl implements IPayProductService { term.setDictValue(termKey(product)); term.setVersionNo(product.getVersionNo() == null ? 1 : product.getVersionNo()); - term.setStatus("0"); + term.setStatus(TERM_EFFECT); term.setCreateBy(operator); payProductTermMapper.insertTerm(term); } @@ -535,9 +596,10 @@ public class PayProductServiceImpl implements IPayProductService @Override public int changeTermStatus(PayProductTerm term) { - if (term == null || (!"0".equals(term.getStatus()) && !"1".equals(term.getStatus()))) + if (term == null || (!TERM_PENDING.equals(term.getStatus()) && !TERM_EFFECT.equals(term.getStatus()) + && !TERM_EXPIRED.equals(term.getStatus()))) { - throw new ServiceException("请选择生效或失效"); + throw new ServiceException("状态只能是待生效、生效或已失效"); } if (term.getTermId() == null && term.getProductId() != null) { @@ -556,27 +618,48 @@ public class PayProductServiceImpl implements IPayProductService } /** 给刚保存的这一版记一条生效记录。新版本从生效日开始,旧版本收到生效日前一天。 */ - private void openTerm(PayProduct product, boolean replacePrevious) + private PayProductTerm openTerm(PayProduct product, boolean replacePrevious, Long priceSetId) { String key = termKey(product); - if (product.getEffectiveDate() == null) + boolean labelMode = priceSetId != null; + // 应付:无生效日则默认今天;打单:创建不写默认生效日,由详情「启动」设置 + if (!labelMode && product.getEffectiveDate() == null) { product.setEffectiveDate(today()); } if (replacePrevious) { - java.time.LocalDate start = toLocalDate(product.getEffectiveDate()); - payProductTermMapper.closeOpenTerms(key, toDate(start.minusDays(1)), product.getCreateBy()); + Date endDate; + if (product.getEffectiveDate() != null) + { + endDate = toDate(toLocalDate(product.getEffectiveDate()).minusDays(1)); + } + else + { + endDate = toDate(toLocalDate(today()).minusDays(1)); + } + if (labelMode) + { + payProductTermMapper.closeOpenTermsBySetProduct(priceSetId, product.getProductId(), endDate, product.getCreateBy()); + } + else + { + payProductTermMapper.closeOpenTerms(key, endDate, product.getCreateBy()); + } } PayProductTerm term = new PayProductTerm(); term.setDictValue(key); term.setProductId(product.getProductId()); + term.setPriceSetId(priceSetId); term.setVersionNo(product.getVersionNo() == null ? 1 : product.getVersionNo()); - term.setEffectiveStart(product.getEffectiveDate()); + term.setEffectiveStart(labelMode ? null : product.getEffectiveDate()); term.setFilePath(sourcePath(product.getFilePath())); - term.setStatus("0"); + // 打单套餐新建为待生效,详情启动后再变生效;应付公共模板直接生效 + term.setStatus(labelMode ? TERM_PENDING : TERM_EFFECT); term.setCreateBy(product.getCreateBy()); payProductTermMapper.insertTerm(term); + product.setTermId(term.getTermId()); + return term; } private String termKey(PayProduct product) @@ -730,6 +813,20 @@ public class PayProductServiceImpl implements IPayProductService { return null; } + PayProductTerm term = payProductTermMapper.selectByProductId(productId); + if (term != null) + { + product.setTermId(term.getTermId()); + product.setTermStatus(term.getStatus()); + product.setTermStart(term.getEffectiveStart()); + product.setTermEnd(term.getEffectiveEnd()); + product.setVersionNo(term.getVersionNo() == null ? product.getVersionNo() : term.getVersionNo()); + List termIds = Collections.singletonList(term.getTermId()); + product.setWeightBands(payProductMapper.selectWeightBandsByTerm(termIds)); + product.setSurcharges(payProductMapper.selectSurchargesByTerm(termIds)); + product.setExtraLines(payProductMapper.selectExtraLinesByTerm(termIds)); + return product; + } List ids = Collections.singletonList(productId); product.setWeightBands(payProductMapper.selectWeightBands(ids)); product.setSurcharges(payProductMapper.selectSurcharges(ids)); @@ -737,6 +834,450 @@ public class PayProductServiceImpl implements IPayProductService return product; } + @Override + public PayProduct selectPayProductDetailByTerm(Long termId) + { + if (termId == null) + { + return null; + } + PayProductTerm term = payProductTermMapper.selectById(termId); + if (term == null || term.getProductId() == null) + { + return null; + } + PayProduct product = payProductMapper.selectPayProductById(term.getProductId()); + if (product == null) + { + return null; + } + product.setTermId(term.getTermId()); + product.setTermStatus(term.getStatus()); + product.setTermStart(term.getEffectiveStart()); + product.setTermEnd(term.getEffectiveEnd()); + product.setVersionNo(term.getVersionNo() == null ? product.getVersionNo() : term.getVersionNo()); + if (!isBlank(term.getFilePath())) + { + product.setFilePath(term.getFilePath()); + } + List termIds = Collections.singletonList(termId); + product.setWeightBands(payProductMapper.selectWeightBandsByTerm(termIds)); + product.setSurcharges(payProductMapper.selectSurchargesByTerm(termIds)); + product.setExtraLines(payProductMapper.selectExtraLinesByTerm(termIds)); + return product; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String saveLabel(PaySaveCommand command) + { + if (command == null || command.getProducts() == null || command.getProducts().isEmpty()) + { + throw new ServiceException("请先勾选要保存的产品"); + } + Long priceSetId = ensureLabelPriceSet(command); + String operator = SecurityUtils.getUsername(); + Date keepStart = null; + String keepStatus = TERM_PENDING; + int nextVersionNo; + boolean editPendingVersion = command.getVersionNo() != null; + if (editPendingVersion) + { + // 编辑待生效版:清掉该 version 后按原号重写,保留原生效日 + LabelVersionKeep keep = clearPendingLabelVersion(priceSetId, command.getVersionNo(), operator); + keepStart = keep.effectiveStart; + keepStatus = keep.status == null ? TERM_PENDING : keep.status; + nextVersionNo = command.getVersionNo(); + } + else + { + // 整批统一套餐级 version_no:新建套餐从 1,新建版本 = max+1 + Integer maxSetVer = payProductTermMapper.selectMaxVersionByPriceSet(priceSetId); + nextVersionNo = (maxSetVer == null ? 0 : maxSetVer) + 1; + } + int saved = 0; + int replaced = 0; + int skipped = 0; + for (PayProduct product : command.getProducts()) + { + if (product == null || isBlank(product.getProductName()) || isBlank(product.getSheetName()) || isBlank(product.getFileMd5())) + { + throw new ServiceException("产品名称、工作表和文件摘要不能为空"); + } + product.setProductType(PAY_TYPE_AR); + String dict = blankTo(product.getDictValue(), product.getProductName()); + product.setDictValue(dict); + // 独立产品族:只复用打单侧 AR,绝不复用/改写应付公共模板 + Long existsId = isBlank(dict) ? null : payProductMapper.selectLabelProductIdByDict(dict); + // 编辑待生效版时强制覆盖;新建版本仅「待生效」占用时跳过 + boolean hasPending = !editPendingVersion && existsId != null + && hasLabelTermStatus(priceSetId, existsId, TERM_PENDING); + if (hasPending && !command.isOverwrite()) + { + skipped++; + continue; + } + product.setFilePath(resolveSource(product)); + prepareCounts(product); + fitColumns(product); + product.setChannelId(null); + product.setCreateBy(operator); + product.setUpdateBy(operator); + product.setCreateTime(new Date()); + product.setVersionNo(nextVersionNo); + // 打单不写默认生效日,由详情设置;编辑时下面统一回写 keepStart + product.setEffectiveDate(null); + if (existsId != null) + { + appendLabelVersion(existsId, product, operator, priceSetId, nextVersionNo, + editPendingVersion || command.isOverwrite()); + replaced++; + } + else + { + // 新建打单专用产品行,与应付同名字典产品并存互不影响 + product.setProductId(null); + product.setOriginId(null); + payProductMapper.insertPayProduct(product); + openTerm(product, false, priceSetId); + insertBands(product); + insertSurcharges(product); + insertExtras(product); + } + saved++; + } + if (editPendingVersion && keepStart != null) + { + payProductTermMapper.updateLabelVersionPeriod(priceSetId, nextVersionNo, keepStart, null, keepStatus, operator); + } + return editPendingVersion + ? ("套餐" + priceSetId + "版本V" + nextVersionNo + "已更新" + saved + "个产品") + : ("套餐" + priceSetId + "已保存" + saved + "个产品,新版本" + replaced + "个,跳过" + skipped + "个"); + } + + private static final class LabelVersionKeep + { + private Date effectiveStart; + private String status; + } + + /** 仅允许清待生效版;返回原生效日便于编辑后回写 */ + private LabelVersionKeep clearPendingLabelVersion(Long priceSetId, Integer versionNo, String operator) + { + if (priceSetId == null || versionNo == null) + { + throw new ServiceException("请指定要编辑的版本"); + } + List terms = payProductTermMapper.selectByPriceSetIdAndVersion(priceSetId, versionNo); + if (terms == null || terms.isEmpty()) + { + throw new ServiceException("该版本不存在或没有产品"); + } + LabelVersionKeep keep = new LabelVersionKeep(); + List termIds = new ArrayList<>(); + for (PayProductTerm term : terms) + { + if (!TERM_PENDING.equals(term.getStatus())) + { + // 兼容:生效日未到但 status 误为 1,仍允许编辑 + boolean futureStart = term.getEffectiveStart() != null + && toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today())); + if (!futureStart) + { + throw new ServiceException("只能编辑待生效版本"); + } + } + if (keep.effectiveStart == null && term.getEffectiveStart() != null) + { + keep.effectiveStart = term.getEffectiveStart(); + } + if (keep.status == null) + { + keep.status = TERM_PENDING; + } + termIds.add(term.getTermId()); + } + payProductMapper.deleteWeightBandsByTerm(termIds); + payProductMapper.deleteSurchargesByTerm(termIds); + payProductMapper.deleteExtraLinesByTerm(termIds); + payProductTermMapper.deleteByTermIds(termIds); + return keep; + } + + @Override + public List selectLabelProducts(Long priceSetId) + { + return toLabelProducts(payProductTermMapper.selectByPriceSetId(priceSetId)); + } + + @Override + public List selectLabelProductsByVersion(Long priceSetId, Integer versionNo) + { + if (priceSetId == null || versionNo == null) + { + return Collections.emptyList(); + } + return toLabelProducts(payProductTermMapper.selectByPriceSetIdAndVersion(priceSetId, versionNo)); + } + + private List toLabelProducts(List terms) + { + if (terms == null || terms.isEmpty()) + { + return Collections.emptyList(); + } + List list = new ArrayList<>(); + for (PayProductTerm term : terms) + { + PayProduct product = payProductMapper.selectPayProductById(term.getProductId()); + if (product == null) + { + continue; + } + product.setTermId(term.getTermId()); + product.setTermStatus(term.getStatus()); + product.setTermStart(term.getEffectiveStart()); + product.setTermEnd(term.getEffectiveEnd()); + product.setVersionNo(term.getVersionNo() == null ? product.getVersionNo() : term.getVersionNo()); + product.setSourceFile(blankTo(term.getSourceFile(), product.getSourceFile())); + if (!isBlank(term.getFilePath())) + { + product.setFilePath(term.getFilePath()); + } + product.setWeightCount(term.getWeightCount() == null ? product.getWeightCount() : term.getWeightCount()); + product.setSurchargeCount(term.getSurchargeCount() == null ? product.getSurchargeCount() : term.getSurchargeCount()); + list.add(product); + } + return list; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int removeLabelProducts(Long priceSetId, Long[] productIds) + { + if (priceSetId == null || productIds == null || productIds.length == 0) + { + return 0; + } + int n = 0; + List allTerms = payProductTermMapper.selectByPriceSetId(priceSetId); + if (allTerms == null || allTerms.isEmpty()) + { + return 0; + } + Set removeIds = new LinkedHashSet<>(Arrays.asList(productIds)); + List termIds = new ArrayList<>(); + for (PayProductTerm term : allTerms) + { + if (term.getProductId() != null && removeIds.contains(term.getProductId())) + { + termIds.add(term.getTermId()); + } + } + if (termIds.isEmpty()) + { + return 0; + } + payProductMapper.deleteWeightBandsByTerm(termIds); + payProductMapper.deleteSurchargesByTerm(termIds); + payProductMapper.deleteExtraLinesByTerm(termIds); + payProductTermMapper.deleteByTermIds(termIds); + return termIds.size(); + } + + private boolean hasActiveLabelTerm(Long priceSetId, Long productId) + { + return hasLabelTermStatus(priceSetId, productId, TERM_PENDING) + || hasLabelTermStatus(priceSetId, productId, TERM_EFFECT); + } + + private boolean hasLabelTermStatus(Long priceSetId, Long productId, String status) + { + List terms = payProductTermMapper.selectByPriceSetId(priceSetId); + if (terms == null || productId == null || status == null) + { + return false; + } + for (PayProductTerm term : terms) + { + if (productId.equals(term.getProductId()) && status.equals(term.getStatus())) + { + return true; + } + } + return false; + } + + /** + * 打单追加版本:不关闭已生效 term,只在覆盖时关掉待生效;version_no 用套餐级批次号。 + */ + private PayProduct appendLabelVersion(Long rootId, PayProduct incoming, String operator, Long priceSetId, + int versionNo, boolean overwritePending) + { + Long root = resolveRoot(rootId); + PayProduct base = payProductMapper.selectPayProductById(root); + PayProduct copy = copyHeader(base, incoming == null ? base : incoming); + copy.setProductId(root); + copy.setOriginId(null); + copy.setChannelId(null); + copy.setDictValue(blankTo(copy.getDictValue(), base.getDictValue())); + copy.setVersionNo(versionNo); + copy.setEffectiveDate(null); + copy.setCreateBy(operator); + copy.setUpdateBy(operator); + copy.setCreateTime(new Date()); + prepareCounts(copy); + fitColumns(copy); + payProductMapper.updatePayProduct(copy); + if (overwritePending) + { + Date endDate = toDate(toLocalDate(today()).minusDays(1)); + payProductTermMapper.closePendingBySetProduct(priceSetId, root, endDate, operator); + } + openTerm(copy, false, priceSetId); + insertBands(copy); + insertSurcharges(copy); + insertExtras(copy); + return selectPayProductDetailByTerm(copy.getTermId()); + } + + private Long ensureLabelPriceSet(PaySaveCommand command) + { + if (command.getPriceSetId() != null) + { + PrcPriceSet db = priceSetMapper.selectPrcPriceSetById(command.getPriceSetId()); + if (db == null) + { + throw new ServiceException("打单套餐不存在"); + } + if (!PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(db.getSetCategory()))) + { + throw new ServiceException("只能向打单套餐导入价卡"); + } + return db.getPriceSetId(); + } + if (isBlank(command.getPriceSetName())) + { + throw new ServiceException("请填写打单套餐名称"); + } + String operator = SecurityUtils.getUsername(); + PrcPriceSet set = new PrcPriceSet(); + set.setPriceSetName(command.getPriceSetName().trim()); + set.setPriceSetNote(command.getPriceSetNote()); + set.setSetType("FIXED"); + set.setSetCategory(PrcPriceSet.CATEGORY_LABEL); + // 新建为待启用,详情页设启动时间后再生效(对齐价格套餐) + set.setStatus("D"); + set.setCreateBy(operator); + set.setUpdateBy(operator); + priceSetMapper.insertPrcPriceSet(set); + set.setPriceSetCode(String.format("LBL%04d", set.getPriceSetId())); + priceSetMapper.updatePrcPriceSet(set); + command.setPriceSetId(set.getPriceSetId()); + return set.getPriceSetId(); + } + + @Override + public List selectLabelVersions(Long priceSetId) + { + if (priceSetId == null) + { + return Collections.emptyList(); + } + PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId); + if (set == null) + { + return Collections.emptyList(); + } + Date now = today(); + // 纠正历史:未来生效版启动时把旧版提前标成已失效 + payProductTermMapper.restorePrematureClosed(priceSetId, now, "system"); + List groups = payProductTermMapper.selectLabelVersionGroups(priceSetId); + if (groups == null || groups.isEmpty()) + { + return Collections.emptyList(); + } + for (LabelPackageVersion ver : groups) + { + ver.setPriceSetId(priceSetId); + ver.setVersionLabel("V" + (ver.getVersionNo() == null ? 1 : ver.getVersionNo())); + int active = ver.getActiveCount() == null ? 0 : ver.getActiveCount(); + int pending = ver.getPendingCount() == null ? 0 : ver.getPendingCount(); + Date start = ver.getEffectiveTime(); + Date end = ver.getExpireTime(); + // 按日期判定:未到生效日=待生效;已过失效日=已失效;中间段=生效 + if (start == null) + { + ver.setStatus(pending > 0 ? TERM_PENDING : (active > 0 ? TERM_EFFECT : TERM_EXPIRED)); + } + else if (toLocalDate(start).isAfter(toLocalDate(now))) + { + ver.setStatus(TERM_PENDING); + } + else if (end != null && toLocalDate(end).isBefore(toLocalDate(now))) + { + ver.setStatus(TERM_EXPIRED); + } + else + { + ver.setStatus(TERM_EFFECT); + } + } + return groups; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String activateLabelVersion(Long priceSetId, Integer versionNo, Date effectiveTime) + { + if (priceSetId == null || versionNo == null) + { + throw new ServiceException("请选择要启动的版本"); + } + if (effectiveTime == null) + { + throw new ServiceException("请选择生效时间"); + } + PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId); + if (set == null) + { + throw new ServiceException("打单套餐不存在"); + } + if (!PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(set.getSetCategory()))) + { + throw new ServiceException("只能启动打单套餐版本"); + } + String operator = SecurityUtils.getUsername(); + boolean future = toLocalDate(effectiveTime).isAfter(toLocalDate(today())); + Date endDate = toDate(toLocalDate(effectiveTime).minusDays(1)); + if (future) + { + // 未到生效日:本版保持待生效;旧版只预约失效日,当前仍生效 + payProductTermMapper.scheduleEndExceptVersion(priceSetId, versionNo, endDate, today(), operator); + } + else + { + // 当天或已过:本版立刻生效,旧版关掉 + payProductTermMapper.closeEffectExceptVersion(priceSetId, versionNo, endDate, operator); + } + String termStatus = future ? TERM_PENDING : TERM_EFFECT; + int n = payProductTermMapper.updateLabelVersionPeriod(priceSetId, versionNo, effectiveTime, null, termStatus, operator); + if (n <= 0) + { + throw new ServiceException("该版本没有可启动的产品价卡"); + } + // 真正生效时才把套餐壳置为生效 + if (!future && !"E".equalsIgnoreCase(String.valueOf(set.getStatus()))) + { + PrcPriceSet upd = new PrcPriceSet(); + upd.setPriceSetId(priceSetId); + upd.setStatus("E"); + upd.setUpdateBy(operator); + priceSetMapper.updatePrcPriceSetStatus(upd); + } + return future ? "已设为待生效" : "版本已启动"; + } + @Override @Transactional(rollbackFor = Exception.class) public int deletePayProducts(Long[] productIds) @@ -826,6 +1367,7 @@ public class PayProductServiceImpl implements IPayProductService for (PayWeightBand row : rows) { row.setProductId(product.getProductId()); + row.setTermId(product.getTermId()); row.setVersionNo(product.getVersionNo() == null ? 1 : product.getVersionNo()); } for (int i = 0; i < rows.size(); i += BATCH) @@ -844,6 +1386,7 @@ public class PayProductServiceImpl implements IPayProductService for (PaySurcharge row : rows) { row.setProductId(product.getProductId()); + row.setTermId(product.getTermId()); row.setVersionNo(product.getVersionNo() == null ? 1 : product.getVersionNo()); } for (int i = 0; i < rows.size(); i += BATCH) @@ -868,6 +1411,7 @@ public class PayProductServiceImpl implements IPayProductService for (PayExtraLine row : rows) { row.setProductId(product.getProductId()); + row.setTermId(product.getTermId()); } for (int i = 0; i < rows.size(); i += BATCH) { @@ -938,7 +1482,7 @@ public class PayProductServiceImpl implements IPayProductService } @Override - public void bindChannel(String channelId, Long productId, Long priceSetId, String payType) + public void bindChannel(String channelId, Long productId, String payType) { if (isBlank(channelId) || productId == null) { @@ -955,7 +1499,6 @@ public class PayProductServiceImpl implements IPayProductService row.setChannelId(channelId); row.setChannelCode(channel.getChannelCode()); row.setProductId(storedId); - row.setPriceSetId(priceSetId); row.setPayType(type); String operator = SecurityUtils.getUsername(); row.setCreateBy(operator); @@ -1015,37 +1558,42 @@ public class PayProductServiceImpl implements IPayProductService return compared; } - /** 在同一个产品编号下追加一版价格,不新开产品编号。 */ + /** 在同一个产品编号下追加一版价格:复用稳定 product_id,明细挂新 term_id。 */ private PayProduct appendVersion(Long rootId, PayProduct incoming, String operator) + { + return appendVersion(rootId, incoming, operator, null); + } + + private PayProduct appendVersion(Long rootId, PayProduct incoming, String operator, Long priceSetId) { Long root = resolveRoot(rootId); PayProduct base = payProductMapper.selectPayProductById(root); PayProduct copy = copyHeader(base, incoming == null ? base : incoming); - copy.setProductId(null); - copy.setOriginId(root); + copy.setProductId(root); + copy.setOriginId(null); copy.setChannelId(null); copy.setDictValue(blankTo(copy.getDictValue(), base.getDictValue())); - Integer maxVersion = payProductMapper.selectMaxVersion(root); + Integer maxVersion = payProductTermMapper.selectMaxVersionByProduct(root, priceSetId); + if (maxVersion == null || maxVersion < 1) + { + maxVersion = payProductMapper.selectMaxVersion(root); + } copy.setVersionNo((maxVersion == null ? 1 : maxVersion) + 1); if (copy.getEffectiveDate() == null) { copy.setEffectiveDate(today()); } copy.setCreateBy(operator); + copy.setUpdateBy(operator); copy.setCreateTime(new Date()); prepareCounts(copy); fitColumns(copy); - payProductMapper.insertPayProduct(copy); + payProductMapper.updatePayProduct(copy); + openTerm(copy, true, priceSetId); insertBands(copy); insertSurcharges(copy); insertExtras(copy); - openTerm(copy, true); - PayProduct saved = selectPayProductDetail(copy.getProductId()); - if (saved != null) - { - saved.setOriginId(root); - } - return saved; + return selectPayProductDetailByTerm(copy.getTermId()); } /** 渠道产品绑定记在映射表,产品编号是不变的根产品。 */ 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 c8b15e81..a9030edf 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 @@ -77,6 +77,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService 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 CATEGORY_LABEL = PrcPriceSet.CATEGORY_LABEL; private static final String TYPE_DISCOUNT = "DISCOUNT"; private static final String VER_DRAFT = "0"; private static final String VER_EFFECT = "1"; @@ -2979,16 +2980,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService /** PACKAGE (default) or EXCLUSIVE. */ public static String normalizeSetCategory(String setCategory) { - if (CATEGORY_EXCLUSIVE.equalsIgnoreCase(StringUtils.trim(setCategory))) + String raw = StringUtils.trim(setCategory); + if (CATEGORY_EXCLUSIVE.equalsIgnoreCase(raw)) { return CATEGORY_EXCLUSIVE; } + if (CATEGORY_LABEL.equalsIgnoreCase(raw)) + { + return CATEGORY_LABEL; + } return CATEGORY_PACKAGE; } private static String buildPriceSetCode(String setCategory, Long priceSetId) { - String prefix = CATEGORY_EXCLUSIVE.equals(normalizeSetCategory(setCategory)) ? "EXC" : "SET"; + String category = normalizeSetCategory(setCategory); + String prefix = CATEGORY_EXCLUSIVE.equals(category) ? "EXC" + : (CATEGORY_LABEL.equals(category) ? "LBL" : "SET"); return String.format("%s%04d", prefix, priceSetId); } diff --git a/bms-back/bms-system/src/main/resources/mapper/price/ChannelProductMapMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/ChannelProductMapMapper.xml index 72ec12d2..2d5157fd 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/ChannelProductMapMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/ChannelProductMapMapper.xml @@ -6,7 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select product_id from pay_product where dict_value = #{dictValue} and (channel_id is null or channel_id = '') and origin_id is null + order by product_id limit 1 + + + + + @@ -238,11 +314,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" insert into pay_weight_band ( - product_id, version_no, section_name, country_name, country_code, zone_name, weight_label, weight_min, weight_max, + product_id, term_id, version_no, section_name, country_name, country_code, zone_name, weight_label, weight_min, weight_max, volume_text, charge_name, price, price_unit, sort_no ) values - (#{row.productId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.countryName}, #{row.countryCode}, #{row.zoneName}, + (#{row.productId}, #{row.termId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.countryName}, #{row.countryCode}, #{row.zoneName}, #{row.weightLabel}, #{row.weightMin}, #{row.weightMax}, #{row.volumeText}, #{row.chargeName}, #{row.price}, #{row.priceUnit}, #{row.sortNo}) @@ -250,11 +326,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" insert into pay_surcharge ( - product_id, version_no, section_name, fee_name, zone_name, charge_text, amount, currency, price_unit, fee_desc, period_text, + product_id, term_id, version_no, section_name, fee_name, zone_name, charge_text, amount, currency, price_unit, fee_desc, period_text, effective_start, effective_end, charge_timing, calc_rule, calc_param, weight_min, weight_max, postcode_param, sort_no ) values - (#{row.productId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.feeName}, #{row.zoneName}, #{row.chargeText}, #{row.amount}, + (#{row.productId}, #{row.termId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.feeName}, #{row.zoneName}, #{row.chargeText}, #{row.amount}, #{row.currency}, #{row.priceUnit}, #{row.feeDesc}, #{row.periodText}, #{row.effectiveStart}, #{row.effectiveEnd}, #{row.chargeTiming}, ifnull(#{row.calcRule}, 'MANUAL'), #{row.calcParam}, #{row.weightMin}, #{row.weightMax}, #{row.postcodeParam}, #{row.sortNo}) @@ -262,14 +338,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - insert into pay_extra_line (product_id, line_type, line_text, sort_no) values + insert into pay_extra_line (product_id, term_id, line_type, line_text, sort_no) values - (#{row.productId}, #{row.lineType}, #{row.lineText}, #{row.sortNo}) + (#{row.productId}, #{row.termId}, #{row.lineType}, #{row.lineText}, #{row.sortNo}) + + + + + + + + delete from pay_weight_band where term_id in + #{id} + + + delete from pay_surcharge where term_id in + #{id} + + + delete from pay_extra_line where term_id in + #{id} + + delete from pay_weight_band where product_id in #{id} @@ -323,13 +442,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where product_id = #{rootId} or origin_id = #{rootId} - + - select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.version_no as versionNo, + select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.price_set_id as priceSetId, + t.version_no as versionNo, t.effective_start as effectiveStart, t.effective_end as effectiveEnd, t.status, t.created_by as createBy, t.create_time as createTime, t.remark, p.product_name as productName, p.source_file as sourceFile, @@ -15,29 +16,107 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" join pay_product p on p.product_id = t.product_id where t.dict_value = #{dictValue} and (p.channel_id is null or p.channel_id = '') + and t.price_set_id is null order by t.version_no desc, t.term_id desc + + + + + + + + + + + update pay_product_term + set effective_start = #{effectiveStart}, + effective_end = #{effectiveEnd}, + status = #{status}, + updated_by = #{updateBy}, + update_time = sysdate() + where price_set_id = #{priceSetId} + and version_no = #{versionNo} + and status in ('0', '1') + + + + + + insert into pay_product_term ( - dict_value, product_id, version_no, effective_start, effective_end, file_path, status, created_by, create_time, remark + dict_value, product_id, price_set_id, version_no, effective_start, effective_end, file_path, status, created_by, create_time, remark ) values ( - #{dictValue}, #{productId}, ifnull(#{versionNo}, 1), #{effectiveStart}, #{effectiveEnd}, #{filePath}, + #{dictValue}, #{productId}, #{priceSetId}, ifnull(#{versionNo}, 1), #{effectiveStart}, #{effectiveEnd}, #{filePath}, ifnull(#{status}, '0'), #{createBy}, sysdate(), #{remark} ) @@ -59,9 +138,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where term_id = #{termId} + update pay_product_term - set status = '1', + set status = '2', effective_end = case when effective_end is not null and effective_end < #{endDate} then effective_end when effective_start is not null and effective_start > #{endDate} then effective_start @@ -70,14 +150,90 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" updated_by = #{updateBy}, update_time = sysdate() where dict_value = #{dictValue} + and status in ('0', '1') + and price_set_id is null + + + + update pay_product_term + set status = '2', + effective_end = case + when effective_end is not null and effective_end < #{endDate} then effective_end + when effective_start is not null and effective_start > #{endDate} then effective_start + else #{endDate} + end, + updated_by = #{updateBy}, + update_time = sysdate() + where product_id = #{productId} + and price_set_id = #{priceSetId} + and status in ('0', '1') + + + + update pay_product_term + set status = '2', + effective_end = case + when effective_end is not null and effective_end < #{endDate} then effective_end + when effective_start is not null and effective_start > #{endDate} then effective_start + else #{endDate} + end, + updated_by = #{updateBy}, + update_time = sysdate() + where product_id = #{productId} + and price_set_id = #{priceSetId} and status = '0' + + update pay_product_term + set status = '2', + effective_end = case + when effective_end is not null and effective_end < #{endDate} then effective_end + when effective_start is not null and effective_start > #{endDate} then effective_start + else #{endDate} + end, + updated_by = #{updateBy}, + update_time = sysdate() + where price_set_id = #{priceSetId} + and status = '1' + and version_no <> #{versionNo} + + + + update pay_product_term + set status = '1', + effective_end = #{endDate}, + updated_by = #{updateBy}, + update_time = sysdate() + where price_set_id = #{priceSetId} + and version_no <> #{versionNo} + and ( + status = '1' + or (status = '2' and effective_end is not null and effective_end >= #{today}) + ) + + + + update pay_product_term + set status = '1', + updated_by = #{updateBy}, + update_time = sysdate() + where price_set_id = #{priceSetId} + and status = '2' + and effective_end is not null + and effective_end >= #{today} + + delete from pay_product_term where product_id in #{id} + + delete from pay_product_term where term_id in + #{id} + + update pay_product set effective_date = #{effectiveDate}, 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 59750c4b..0a598966 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 @@ -31,11 +31,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 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) - from prc_price_set_product p - inner join prc_price_set_version v on v.psv_id = p.psv_id - where v.price_set_id = s.price_set_id - and ifnull(p.is_del, '0') = '0') as product_count, + case + when ifnull(s.set_category, 'PACKAGE') = 'LABEL' then + (select count(distinct t.product_id) + from pay_product_term t + where t.price_set_id = s.price_set_id + and t.status = '0') + else + (select count(distinct p.product_code) + from prc_price_set_product p + inner join prc_price_set_version v on v.psv_id = p.psv_id + where v.price_set_id = s.price_set_id + and ifnull(p.is_del, '0') = '0') + end as product_count, (select count(1) from prc_customer_price_set c where c.price_set_id = s.price_set_id) as customer_count, (select group_concat( concat(ifnull(c.customer_code, ''), diff --git a/bms-back/sql/price/label_package.sql b/bms-back/sql/price/label_package.sql new file mode 100644 index 00000000..fe165c2b --- /dev/null +++ b/bms-back/sql/price/label_package.sql @@ -0,0 +1,327 @@ +-- 打单套餐一键脚本(可重复执行) +-- 合并自:pay_product_term.sql + label_package.sql + label_package_create_menu.sql +-- 含:应付有效期表、打单表结构变更、打单/应付相关菜单权限 +SET NAMES utf8mb4; + +-- ========== 一、pay_product_term 建表 + 存量回填 ========== + +CREATE TABLE IF NOT EXISTS `pay_product_term` ( + `term_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '有效期ID', + `dict_value` varchar(100) NOT NULL COMMENT '产品字典值,同一产品的各版本归在一起', + `product_id` bigint(20) NOT NULL COMMENT '这一版价格', + `version_no` int(11) NOT NULL DEFAULT 1 COMMENT '版本号', + `effective_start` date DEFAULT NULL COMMENT '生效开始', + `effective_end` date DEFAULT NULL COMMENT '生效结束,空表示不限', + `file_path` varchar(500) DEFAULT NULL COMMENT '这一版源文件OSS地址,oss://bucket/key', + `status` char(1) NOT NULL DEFAULT '0' COMMENT '0待生效 1生效 2已失效', + `created_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `updated_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`term_id`), + UNIQUE KEY `uk_pay_term_product` (`product_id`), + KEY `idx_pay_term_dict` (`dict_value`, `status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品价格有效期'; + +-- 已有模板补一条生效记录,不覆盖后来手工改过的有效期 +INSERT INTO pay_product_term ( + dict_value, product_id, version_no, effective_start, status, created_by, create_time +) +SELECT ifnull(nullif(dict_value, ''), product_name), product_id, ifnull(version_no, 1), effective_date, '0', + ifnull(created_by, ''), ifnull(create_time, sysdate()) +FROM pay_product p +WHERE (p.channel_id IS NULL OR p.channel_id = '') + AND NOT EXISTS (SELECT 1 FROM pay_product_term t WHERE t.product_id = p.product_id); + +-- ========== 二、打单相关表结构变更 ========== + +-- 0) pay_product_term.status:旧 0生效/1失效 → 新 0待生效/1生效/2已失效 +-- 仅当列注释仍是旧语义时迁移(可重复执行) +SET @old_status_comment := ( + SELECT COUNT(1) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pay_product_term' + AND COLUMN_NAME = 'status' + AND COLUMN_COMMENT LIKE '%0生效%1失效%' +); +SET @sql := IF(@old_status_comment > 0, + 'UPDATE pay_product_term SET status = ''2'' WHERE status = ''1''', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @sql := IF(@old_status_comment > 0, + 'UPDATE pay_product_term SET status = ''1'' WHERE status = ''0''', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +ALTER TABLE pay_product_term + MODIFY COLUMN status char(1) NOT NULL DEFAULT '0' COMMENT '0待生效 1生效 2已失效'; + +-- 1) prc_price_set.set_category 注释增加 LABEL +ALTER TABLE prc_price_set + MODIFY COLUMN set_category varchar(20) NOT NULL DEFAULT 'PACKAGE' + COMMENT '分类 PACKAGE=普通套餐 EXCLUSIVE=专属报价 LABEL=打单套餐'; + +-- 2) pay_product_term.price_set_id +SET @exists := ( + SELECT COUNT(1) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pay_product_term' + AND COLUMN_NAME = 'price_set_id' +); +SET @sql := IF(@exists = 0, + 'ALTER TABLE pay_product_term + ADD COLUMN price_set_id bigint(20) DEFAULT NULL + COMMENT ''打单套餐ID,对应 prc_price_set.price_set_id;应付公共模板为空'' + AFTER product_id, + ADD KEY idx_pay_term_price_set (price_set_id)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 3) 去掉一产品一条 term 的唯一约束,支持一产品多周期 +SET @uk := ( + SELECT COUNT(1) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pay_product_term' + AND INDEX_NAME = 'uk_pay_term_product' +); +SET @sql := IF(@uk > 0, 'ALTER TABLE pay_product_term DROP INDEX uk_pay_term_product', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @idx1 := ( + SELECT COUNT(1) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pay_product_term' + AND INDEX_NAME = 'idx_pay_term_set_product_time' +); +SET @sql := IF(@idx1 = 0, + 'ALTER TABLE pay_product_term ADD KEY idx_pay_term_set_product_time (price_set_id, product_id, effective_start, effective_end)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @idx2 := ( + SELECT COUNT(1) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pay_product_term' + AND INDEX_NAME = 'idx_pay_term_product_time' +); +SET @sql := IF(@idx2 = 0, + 'ALTER TABLE pay_product_term ADD KEY idx_pay_term_product_time (product_id, effective_start, effective_end)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 4) 明细表增加 term_id +SET @exists := ( + SELECT COUNT(1) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_weight_band' AND COLUMN_NAME = 'term_id' +); +SET @sql := IF(@exists = 0, + 'ALTER TABLE pay_weight_band + ADD COLUMN term_id bigint(20) DEFAULT NULL + COMMENT ''价卡版本周期ID,对应 pay_product_term.term_id;与 product_id 组成明细唯一维度'' + AFTER product_id, + ADD KEY idx_pay_weight_product_term (product_id, term_id)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @exists := ( + SELECT COUNT(1) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_surcharge' AND COLUMN_NAME = 'term_id' +); +SET @sql := IF(@exists = 0, + 'ALTER TABLE pay_surcharge + ADD COLUMN term_id bigint(20) DEFAULT NULL + COMMENT ''价卡版本周期ID,对应 pay_product_term.term_id;与 product_id 组成明细唯一维度'' + AFTER product_id, + ADD KEY idx_pay_surcharge_product_term (product_id, term_id)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @exists := ( + SELECT COUNT(1) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_extra_line' AND COLUMN_NAME = 'term_id' +); +SET @sql := IF(@exists = 0, + 'ALTER TABLE pay_extra_line + ADD COLUMN term_id bigint(20) DEFAULT NULL + COMMENT ''价卡版本周期ID,对应 pay_product_term.term_id;与 product_id 组成明细唯一维度'' + AFTER product_id, + ADD KEY idx_pay_extra_product_term (product_id, term_id)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 5) 存量明细回填 term_id(一产品多 term 时取最新一条) +UPDATE pay_weight_band b +INNER JOIN ( + SELECT t1.product_id, t1.term_id + FROM pay_product_term t1 + INNER JOIN ( + SELECT product_id, MAX(term_id) AS term_id + FROM pay_product_term + GROUP BY product_id + ) t2 ON t2.product_id = t1.product_id AND t2.term_id = t1.term_id +) t ON t.product_id = b.product_id +SET b.term_id = t.term_id +WHERE b.term_id IS NULL; + +UPDATE pay_surcharge s +INNER JOIN ( + SELECT t1.product_id, t1.term_id + FROM pay_product_term t1 + INNER JOIN ( + SELECT product_id, MAX(term_id) AS term_id + FROM pay_product_term + GROUP BY product_id + ) t2 ON t2.product_id = t1.product_id AND t2.term_id = t1.term_id +) t ON t.product_id = s.product_id +SET s.term_id = t.term_id +WHERE s.term_id IS NULL; + +UPDATE pay_extra_line e +INNER JOIN ( + SELECT t1.product_id, t1.term_id + FROM pay_product_term t1 + INNER JOIN ( + SELECT product_id, MAX(term_id) AS term_id + FROM pay_product_term + GROUP BY product_id + ) t2 ON t2.product_id = t1.product_id AND t2.term_id = t1.term_id +) t ON t.product_id = e.product_id +SET e.term_id = t.term_id +WHERE e.term_id IS NULL; + +-- 6) 删除渠道映射上的套餐字段 +SET @exists := ( + SELECT COUNT(1) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'srv_channel_product_map' + AND COLUMN_NAME = 'price_set_id' +); +SET @sql := IF(@exists > 0, + 'ALTER TABLE srv_channel_product_map DROP INDEX idx_channel_product_map_price_set, DROP COLUMN price_set_id', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- ========== 三、应付配置菜单(price:pay:edit) ========== + +SET @menuId := (SELECT menu_id FROM sys_menu WHERE perms = 'price:pay:list' LIMIT 1); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT '应付配置修改', @menuId, 5, '', '', 1, 0, 'F', '0', '0', 'price:pay:edit', '#', 'admin', sysdate() +WHERE @menuId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:pay:edit'); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT rm.role_id, m.menu_id +FROM sys_role_menu rm +JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'price:pay:list' +JOIN sys_menu m ON m.perms = 'price:pay:edit' +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id +); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id +FROM sys_menu m +WHERE m.perms = 'price:pay:edit' + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu x WHERE x.role_id = 1 AND x.menu_id = m.menu_id + ); + +-- ========== 四、打单套餐菜单权限(挂在套餐管理下) ========== + +SET @pkgParentId := ( + SELECT menu_id FROM sys_menu + WHERE menu_id = 2200 + OR (menu_name = N'套餐管理' AND parent_id = 0 AND menu_type = 'M') + ORDER BY CASE WHEN menu_id = 2200 THEN 0 ELSE 1 END, menu_id + LIMIT 1 +); + +-- 列表 +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2620, N'打单套餐', @pkgParentId, 3, 'label', 'price/label/labelPackage', '', 'LabelPackage', 1, 0, 'C', '0', '0', 'price:label:list', 'list', 'admin', sysdate(), N'打单套餐列表' +FROM DUAL WHERE @pkgParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2620 OR perms = 'price:label:list'); + +UPDATE sys_menu +SET menu_name = N'打单套餐', + parent_id = @pkgParentId, + order_num = 3, + path = 'label', + component = 'price/label/labelPackage', + route_name = 'LabelPackage', + perms = 'price:label:list', + remark = N'打单套餐列表', + update_by = 'admin', + update_time = sysdate() +WHERE @pkgParentId IS NOT NULL AND (menu_id = 2620 OR perms = 'price:label:list'); + +-- 隐藏新建页 +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 2628, N'打单套餐新建', @pkgParentId, 7, 'labelCreate', 'price/label/create', '', 'LabelPackageCreate', 1, 0, 'C', '1', '0', 'price:label:add', '#', 'admin', sysdate(), N'打单套餐新建(基本信息+Excel导入)' +FROM DUAL WHERE @pkgParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2628 OR path = 'labelCreate'); + +UPDATE sys_menu +SET menu_name = N'打单套餐新建', + parent_id = @pkgParentId, + order_num = 7, + path = 'labelCreate', + component = 'price/label/create', + route_name = 'LabelPackageCreate', + perms = 'price:label:add', + visible = '1', + remark = N'打单套餐新建(基本信息+Excel导入)', + update_by = 'admin', + update_time = sysdate() +WHERE @pkgParentId IS NOT NULL AND (menu_id = 2628 OR path = 'labelCreate'); + +-- 隐藏详情页 +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 2627, N'打单套餐详情', @pkgParentId, 8, 'labelDetail', 'price/label/detail', '', 'LabelPackageDetail', 1, 0, 'C', '1', '0', 'price:label:query', '#', 'admin', sysdate(), N'打单套餐产品明细' +FROM DUAL WHERE @pkgParentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2627 OR path = 'labelDetail'); + +UPDATE sys_menu +SET menu_name = N'打单套餐详情', + parent_id = @pkgParentId, + order_num = 8, + path = 'labelDetail', + component = 'price/label/detail', + route_name = 'LabelPackageDetail', + perms = 'price:label:query', + visible = '1', + remark = N'打单套餐产品明细', + update_by = 'admin', + update_time = sysdate() +WHERE @pkgParentId IS NOT NULL AND (menu_id = 2627 OR path = 'labelDetail'); + +-- 按钮 +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 2621, N'查询', 2620, 1, '', NULL, 1, 0, 'F', '0', '0', 'price:label:query', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2621 OR (parent_id = 2620 AND perms = 'price:label: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 2622, N'新增', 2620, 2, '', NULL, 1, 0, 'F', '0', '0', 'price:label:add', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2622 OR (parent_id = 2620 AND perms = 'price:label: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 2623, N'修改', 2620, 3, '', NULL, 1, 0, 'F', '0', '0', 'price:label:edit', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2623 OR (parent_id = 2620 AND perms = 'price:label: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 2624, N'删除', 2620, 4, '', NULL, 1, 0, 'F', '0', '0', 'price:label:remove', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2624 OR (parent_id = 2620 AND perms = 'price:label: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 2625, N'导入', 2620, 5, '', NULL, 1, 0, 'F', '0', '0', 'price:label:import', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2625 OR (parent_id = 2620 AND perms = 'price:label:import' 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 2626, N'启停', 2620, 6, '', NULL, 1, 0, 'F', '0', '0', 'price:label:audit', '#', 'admin', sysdate(), N'' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2626 OR (parent_id = 2620 AND perms = 'price:label:audit' AND menu_type = 'F')); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id FROM ( + SELECT 2620 AS menu_id UNION SELECT 2621 UNION SELECT 2622 + UNION SELECT 2623 UNION SELECT 2624 UNION SELECT 2625 UNION SELECT 2626 + UNION SELECT 2627 UNION SELECT 2628 +) 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-back/sql/price/pay_product_term.sql b/bms-back/sql/price/pay_product_term.sql deleted file mode 100644 index 145b832d..00000000 --- a/bms-back/sql/price/pay_product_term.sql +++ /dev/null @@ -1,54 +0,0 @@ --- 应付产品有效期。同一产品多次导入各留一版,用这张表记录生效区间和是否生效。可重复执行。 -SET NAMES utf8mb4; - -CREATE TABLE IF NOT EXISTS `pay_product_term` ( - `term_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '有效期ID', - `dict_value` varchar(100) NOT NULL COMMENT '产品字典值,同一产品的各版本归在一起', - `product_id` bigint(20) NOT NULL COMMENT '这一版价格', - `version_no` int(11) NOT NULL DEFAULT 1 COMMENT '版本号', - `effective_start` date DEFAULT NULL COMMENT '生效开始', - `effective_end` date DEFAULT NULL COMMENT '生效结束,空表示不限', - `file_path` varchar(500) DEFAULT NULL COMMENT '这一版源文件OSS地址,oss://bucket/key', - `status` char(1) NOT NULL DEFAULT '0' COMMENT '0生效 1失效', - `created_by` varchar(64) DEFAULT '' COMMENT '创建者', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `updated_by` varchar(64) DEFAULT '' COMMENT '更新者', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) DEFAULT NULL COMMENT '备注', - PRIMARY KEY (`term_id`), - UNIQUE KEY `uk_pay_term_product` (`product_id`), - KEY `idx_pay_term_dict` (`dict_value`, `status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品价格有效期'; - --- 已有模板补一条生效记录,不覆盖后来手工改过的有效期 -INSERT INTO pay_product_term ( - dict_value, product_id, version_no, effective_start, status, created_by, create_time -) -SELECT ifnull(nullif(dict_value, ''), product_name), product_id, ifnull(version_no, 1), effective_date, '0', - ifnull(created_by, ''), ifnull(create_time, sysdate()) -FROM pay_product p -WHERE (p.channel_id IS NULL OR p.channel_id = '') - AND NOT EXISTS (SELECT 1 FROM pay_product_term t WHERE t.product_id = p.product_id); - -SET @menuId := (SELECT menu_id FROM sys_menu WHERE perms = 'price:pay:list' LIMIT 1); - -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) -SELECT '应付配置修改', @menuId, 5, '', '', 1, 0, 'F', '0', '0', 'price:pay:edit', '#', 'admin', sysdate() -WHERE @menuId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:pay:edit'); - -INSERT INTO sys_role_menu (role_id, menu_id) -SELECT DISTINCT rm.role_id, m.menu_id -FROM sys_role_menu rm -JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'price:pay:list' -JOIN sys_menu m ON m.perms = 'price:pay:edit' -WHERE NOT EXISTS ( - SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id -); - -INSERT INTO sys_role_menu (role_id, menu_id) -SELECT 1, m.menu_id -FROM sys_menu m -WHERE m.perms = 'price:pay:edit' - AND NOT EXISTS ( - SELECT 1 FROM sys_role_menu x WHERE x.role_id = 1 AND x.menu_id = m.menu_id - ); diff --git a/bms-front/src/api/price/label.js b/bms-front/src/api/price/label.js new file mode 100644 index 00000000..688699f0 --- /dev/null +++ b/bms-front/src/api/price/label.js @@ -0,0 +1,156 @@ +import request from '@/utils/request' + +export function listLabelPackage(query) { + return request({ + url: '/price/label/list', + method: 'get', + params: query + }) +} + +export function getLabelPackage(priceSetId) { + return request({ + url: '/price/label/' + priceSetId, + method: 'get' + }) +} + +export function updateLabelBasic(data) { + return request({ + url: '/price/label/basic', + method: 'put', + data + }) +} + +export function disableLabelPackage(priceSetId) { + return request({ + url: '/price/label/' + priceSetId + '/disable', + method: 'put' + }) +} + +export function enableLabelPackage(priceSetId) { + return request({ + url: '/price/label/' + priceSetId + '/enable', + method: 'put' + }) +} + +export function delLabelPackage(priceSetIds) { + return request({ + url: '/price/label/' + priceSetIds, + method: 'delete' + }) +} + +export function previewLabel(file, dictValue, progressId, onUploadProgress, priceSetId) { + const data = new FormData() + data.append('file', file) + if (dictValue) data.append('dictValue', dictValue) + if (progressId) data.append('progressId', progressId) + if (priceSetId) data.append('priceSetId', priceSetId) + return request({ + url: '/price/label/preview', + method: 'post', + data, + headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }, + timeout: 10 * 60 * 1000, + onUploadProgress + }) +} + +export function labelParseProgress(progressId) { + return request({ + url: '/price/label/preview/progress', + method: 'get', + params: { progressId }, + headers: { repeatSubmit: false } + }) +} + +export function saveLabelImport(data) { + return request({ + url: '/price/label/importSave', + method: 'post', + data, + timeout: 10 * 60 * 1000 + }) +} + +export function listLabelVersions(priceSetId) { + return request({ + url: '/price/label/' + priceSetId + '/versions', + method: 'get' + }) +} + +export function activateLabelVersion(priceSetId, versionNo, data) { + return request({ + url: '/price/label/' + priceSetId + '/versions/' + versionNo + '/activate', + method: 'put', + data + }) +} + +export function listLabelProducts(priceSetId, versionNo) { + return request({ + url: '/price/label/' + priceSetId + '/products', + method: 'get', + params: versionNo != null ? { versionNo } : undefined + }) +} + +export function listLabelCustomer(query) { + return request({ + url: '/price/label/customer/list', + method: 'get', + params: query + }) +} + +export function addLabelCustomer(data) { + return request({ + url: '/price/label/customer', + method: 'post', + data + }) +} + +export function batchAddLabelCustomer(data) { + return request({ + url: '/price/label/customer/batch', + method: 'post', + data + }) +} + +export function updateLabelCustomer(data) { + return request({ + url: '/price/label/customer', + method: 'put', + data + }) +} + +export function delLabelCustomer(ids) { + return request({ + url: '/price/label/customer/' + ids, + method: 'delete' + }) +} + +export function getLabelProduct(productId, termId) { + return request({ + url: '/price/label/product/' + productId, + method: 'get', + params: termId ? { termId } : undefined + }) +} + +export function removeLabelProducts(priceSetId, productIds) { + return request({ + url: '/price/label/' + priceSetId + '/products/' + productIds, + method: 'delete' + }) +} diff --git a/bms-front/src/views/price/basic/pay/index.vue b/bms-front/src/views/price/basic/pay/index.vue index a14001b6..4ddabc29 100644 --- a/bms-front/src/views/price/basic/pay/index.vue +++ b/bms-front/src/views/price/basic/pay/index.vue @@ -40,9 +40,9 @@