From 1a0483c7fee4f651be59aa6a64188aedac63ead9 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Tue, 10 Nov 2026 16:21:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=B8=AA=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../price/LabelPackageController.java | 63 ++++- .../price/PayProductController.java | 43 +++ .../bms/price/domain/ChannelProductMap.java | 9 + .../price/mapper/ChannelProductMapMapper.java | 6 + .../price/mapper/PayProductTermMapper.java | 6 + .../bms/price/service/IPayProductService.java | 16 ++ .../service/impl/PayProductServiceImpl.java | 257 ++++++++++++++++-- .../mapper/price/ChannelProductMapMapper.xml | 28 +- .../mapper/price/PayProductMapper.xml | 12 +- .../mapper/price/PayProductTermMapper.xml | 23 +- bms-front/src/api/price/label.js | 29 ++ bms-front/src/api/price/pay.js | 13 +- .../src/views/data/serviceChannel/index.vue | 83 +++++- .../basic/pay/components/PayConfigDetail.vue | 123 ++++++--- bms-front/src/views/price/basic/pay/index.vue | 45 ++- .../label/components/LabelChannelTable.vue | 182 +++++++++++++ .../components/LabelVersionProductDrawer.vue | 14 +- .../label/components/LabelVersionTable.vue | 38 ++- bms-front/src/views/price/label/create.vue | 61 +++-- bms-front/src/views/price/label/detail.vue | 36 ++- 20 files changed, 940 insertions(+), 147 deletions(-) create mode 100644 bms-front/src/views/price/label/components/LabelChannelTable.vue 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 index 2b3f4fad..14913a99 100644 --- 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 @@ -2,6 +2,7 @@ package com.bms.web.controller.price; import java.util.Date; import java.util.HashMap; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -173,6 +174,15 @@ public class LabelPackageController extends BaseController return success(payload); } + @PreAuthorize("@ss.hasPermi('price:label:remove')") + @Log(title = "打单套餐删除版本", businessType = BusinessType.DELETE) + @DeleteMapping("/{priceSetId}/versions/{versionNo}") + public AjaxResult deleteVersion(@PathVariable Long priceSetId, @PathVariable Integer versionNo) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + return toAjax(payProductService.deleteLabelVersion(priceSetId, versionNo)); + } + @PreAuthorize("@ss.hasPermi('price:label:list')") @GetMapping("/{priceSetId}/products") public AjaxResult products(@PathVariable Long priceSetId, @@ -207,6 +217,55 @@ public class LabelPackageController extends BaseController return toAjax(payProductService.removeLabelProducts(priceSetId, productIds)); } + @PreAuthorize("@ss.hasPermi('price:label:query')") + @GetMapping("/{priceSetId}/channels") + public AjaxResult listChannels(@PathVariable Long priceSetId) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + return success(payProductService.listLabelChannels(priceSetId)); + } + + @PreAuthorize("@ss.hasPermi('price:label:edit')") + @Log(title = "打单套餐绑定渠道", businessType = BusinessType.UPDATE) + @PutMapping("/{priceSetId}/channels") + public AjaxResult bindChannels(@PathVariable Long priceSetId, @RequestBody Map body) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + List channelIds = new ArrayList(); + Object rawIds = body == null ? null : body.get("channelIds"); + if (rawIds instanceof List) + { + for (Object item : (List) rawIds) + { + if (item != null && !String.valueOf(item).trim().isEmpty()) + { + channelIds.add(String.valueOf(item).trim()); + } + } + } + Long productId = null; + Object rawProductId = body == null ? null : body.get("productId"); + if (rawProductId instanceof Number) + { + productId = ((Number) rawProductId).longValue(); + } + else if (rawProductId != null && !String.valueOf(rawProductId).trim().isEmpty() + && !"null".equalsIgnoreCase(String.valueOf(rawProductId).trim())) + { + productId = Long.valueOf(String.valueOf(rawProductId).trim()); + } + return success(payProductService.bindLabelChannels(priceSetId, channelIds, productId)); + } + + @PreAuthorize("@ss.hasPermi('price:label:edit')") + @Log(title = "打单套餐解绑渠道", businessType = BusinessType.DELETE) + @DeleteMapping("/{priceSetId}/channels/{channelId}") + public AjaxResult unbindChannel(@PathVariable Long priceSetId, @PathVariable String channelId) + { + priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL); + return toAjax(payProductService.unbindLabelChannel(priceSetId, channelId)); + } + @PreAuthorize("@ss.hasPermi('price:label:list')") @GetMapping("/customer/list") public TableDataInfo customerList(PrcCustomerPriceSet customer) @@ -289,8 +348,8 @@ public class LabelPackageController extends BaseController { return null; } - // 兼容 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss - if (text.length() >= 19) + // 兼容 yyyy-MM-dd / yyyy-MM-dd HH:mm / yyyy-MM-dd HH:mm:ss + if (text.length() >= 16) { 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 df111c1b..8805dafc 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 @@ -1,6 +1,8 @@ package com.bms.web.controller.price; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Locale; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; @@ -139,6 +141,29 @@ public class PayProductController extends BaseController return success(payProductService.selectChannelPrice(channelId, payType)); } + /** 批量绑定渠道应收/应付产品 */ + @PreAuthorize("@ss.hasAnyPermi('price:pay:edit,data:serviceChannel:edit')") + @Log(title = "渠道批量绑定产品", businessType = BusinessType.UPDATE) + @PutMapping("/channel/batchBind") + public AjaxResult batchBindChannel(@RequestBody Map body) + { + List channelIds = new ArrayList(); + Object rawIds = body == null ? null : body.get("channelIds"); + if (rawIds instanceof List) + { + for (Object item : (List) rawIds) + { + if (item != null && !String.valueOf(item).trim().isEmpty()) + { + channelIds.add(String.valueOf(item).trim()); + } + } + } + Long apProductId = toLong(body == null ? null : body.get("apProductId")); + Long arProductId = toLong(body == null ? null : body.get("arProductId")); + return success(payProductService.batchBindChannel(channelIds, apProductId, arProductId)); + } + /** 对照两个价格版本 */ @PreAuthorize("@ss.hasPermi('price:pay:query')") @GetMapping("/compare") @@ -247,4 +272,22 @@ public class PayProductController extends BaseController throw new ServiceException("价卡文件不能超过30MB"); } } + + private Long toLong(Object raw) + { + if (raw == null) + { + return null; + } + if (raw instanceof Number) + { + return ((Number) raw).longValue(); + } + String text = String.valueOf(raw).trim(); + if (text.isEmpty() || "null".equalsIgnoreCase(text)) + { + return null; + } + return Long.valueOf(text); + } } 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 f3df0c3a..49eb06ad 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,6 +20,15 @@ public class ChannelProductMap private Long productId; + /** 打单套餐 ID,对应 prc_price_set.price_set_id(应收可填) */ + private Long priceSetId; + + /** 渠道名称(列表回显) */ + private String channelName; + + /** 产品名称(列表回显) */ + private String productName; + /** AR 应收,AP 应付 */ private String payType; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/ChannelProductMapMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/ChannelProductMapMapper.java index 03cb50f7..05933715 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/ChannelProductMapMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/ChannelProductMapMapper.java @@ -1,5 +1,6 @@ package com.bms.price.mapper; +import java.util.List; import org.apache.ibatis.annotations.Param; import com.bms.price.domain.ChannelProductMap; @@ -10,9 +11,14 @@ public interface ChannelProductMapMapper { ChannelProductMap selectByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType); + List selectByPriceSetId(@Param("priceSetId") Long priceSetId); + int upsert(ChannelProductMap row); int deleteByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType); + int deleteByChannelTypeAndPriceSet(@Param("channelId") String channelId, @Param("payType") String payType, + @Param("priceSetId") Long priceSetId); + int deleteByProductId(@Param("productId") Long productId); } 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 1190f6e7..f900cf56 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 @@ -17,6 +17,9 @@ public interface PayProductTermMapper PayProductTerm selectByProductId(Long productId); + /** 打单产品所属套餐 ID */ + Long selectLabelPriceSetIdByProduct(@Param("productId") Long productId); + List selectByPriceSetId(@Param("priceSetId") Long priceSetId); List selectByPriceSetIdAndVersion(@Param("priceSetId") Long priceSetId, @@ -68,6 +71,9 @@ public interface PayProductTermMapper int restorePrematureClosed(@Param("priceSetId") Long priceSetId, @Param("today") Date today, @Param("updateBy") String updateBy); + /** 打单:删除待生效版后,清掉旧版预约失效日 */ + int clearScheduledEnd(@Param("priceSetId") Long priceSetId, @Param("updateBy") String updateBy); + int deleteByProductIds(@Param("productIds") List productIds); int deleteByTermIds(@Param("termIds") List termIds); 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 3f8fab55..d5d414f4 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 @@ -7,6 +7,7 @@ import com.bms.price.domain.LabelPackageVersion; import com.bms.price.domain.PayParseProgress; import com.bms.price.domain.PayPreview; import com.bms.price.domain.PayPriceCompare; +import com.bms.price.domain.ChannelProductMap; import com.bms.price.domain.PayProduct; import com.bms.price.domain.PayProductTerm; import com.bms.price.domain.PaySaveCommand; @@ -57,6 +58,9 @@ public interface IPayProductService /** 只绑定产品编号,不产生新版本;同时落 channel_code。payType 为空时按应付 AP */ void bindChannel(String channelId, Long productId, String payType); + /** 批量绑定渠道应收/应付产品;未传的产品不改原绑定 */ + String batchBindChannel(java.util.List channelIds, Long apProductId, Long arProductId); + /** * 打单套餐预览:解析复用应付解析器,但「已存在」只按本套餐 price_set_id 判断, * 不与应付公共模板互相挡导入。 @@ -72,6 +76,9 @@ public interface IPayProductService /** 启动/修改打单套餐某一版生效时间,并视情况启用套餐 */ String activateLabelVersion(Long priceSetId, Integer versionNo, Date effectiveTime); + /** 删除打单套餐待生效版本(仅 status=0) */ + int deleteLabelVersion(Long priceSetId, Integer versionNo); + /** 打单套餐内当前生效/待生效产品列表 */ List selectLabelProducts(Long priceSetId); @@ -84,6 +91,15 @@ public interface IPayProductService /** 从打单套餐移除产品(失效/清理对应 term 明细) */ int removeLabelProducts(Long priceSetId, Long[] productIds); + /** 打单套餐已绑定渠道列表 */ + java.util.List listLabelChannels(Long priceSetId); + + /** 打单套餐批量绑定渠道(写 AR + price_set_id) */ + String bindLabelChannels(Long priceSetId, java.util.List channelIds, Long productId); + + /** 打单套餐解绑渠道 */ + int unbindLabelChannel(Long priceSetId, String channelId); + /** 取消渠道上指定收付类型的产品。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 7cf5a963..83de6020 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 @@ -890,8 +890,8 @@ public class PayProductServiceImpl implements IPayProductService boolean editPendingVersion = command.getVersionNo() != null; if (editPendingVersion) { - // 编辑待生效版:清掉该 version 后按原号重写,保留原生效日 - LabelVersionKeep keep = clearPendingLabelVersion(priceSetId, command.getVersionNo(), operator); + // 编辑待生效/已生效版:清掉该 version 后按原号重写,保留原生效日与状态 + LabelVersionKeep keep = clearEditableLabelVersion(priceSetId, command.getVersionNo(), operator); keepStart = keep.effectiveStart; keepStatus = keep.status == null ? TERM_PENDING : keep.status; nextVersionNo = command.getVersionNo(); @@ -968,8 +968,8 @@ public class PayProductServiceImpl implements IPayProductService private String status; } - /** 仅允许清待生效版;返回原生效日便于编辑后回写 */ - private LabelVersionKeep clearPendingLabelVersion(Long priceSetId, Integer versionNo, String operator) + /** 编辑版本:允许待生效或已生效;返回原生效日与状态便于回写 */ + private LabelVersionKeep clearEditableLabelVersion(Long priceSetId, Integer versionNo, String operator) { if (priceSetId == null || versionNo == null) { @@ -984,15 +984,13 @@ public class PayProductServiceImpl implements IPayProductService List termIds = new ArrayList<>(); for (PayProductTerm term : terms) { - if (!TERM_PENDING.equals(term.getStatus())) + boolean pending = TERM_PENDING.equals(term.getStatus()); + boolean effect = TERM_EFFECT.equals(term.getStatus()); + boolean futureStart = term.getEffectiveStart() != null + && toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today())); + if (!pending && !effect && !futureStart) { - // 兼容:生效日未到但 status 误为 1,仍允许编辑 - boolean futureStart = term.getEffectiveStart() != null - && toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today())); - if (!futureStart) - { - throw new ServiceException("只能编辑待生效版本"); - } + throw new ServiceException("只能编辑待生效或已生效版本"); } if (keep.effectiveStart == null && term.getEffectiveStart() != null) { @@ -1000,7 +998,7 @@ public class PayProductServiceImpl implements IPayProductService } if (keep.status == null) { - keep.status = TERM_PENDING; + keep.status = term.getStatus() == null ? TERM_PENDING : term.getStatus(); } termIds.add(term.getTermId()); } @@ -1011,6 +1009,148 @@ public class PayProductServiceImpl implements IPayProductService return keep; } + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteLabelVersion(Long priceSetId, Integer versionNo) + { + if (priceSetId == null || versionNo == 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("只能删除打单套餐版本"); + } + List terms = payProductTermMapper.selectByPriceSetIdAndVersion(priceSetId, versionNo); + if (terms == null || terms.isEmpty()) + { + throw new ServiceException("该版本不存在或没有产品"); + } + List termIds = new ArrayList<>(); + for (PayProductTerm term : terms) + { + boolean pending = TERM_PENDING.equals(term.getStatus()); + boolean futureStart = term.getEffectiveStart() != null + && toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today())); + if (!pending && !futureStart) + { + throw new ServiceException("只能删除待生效版本"); + } + termIds.add(term.getTermId()); + } + String operator = SecurityUtils.getUsername(); + payProductMapper.deleteWeightBandsByTerm(termIds); + payProductMapper.deleteSurchargesByTerm(termIds); + payProductMapper.deleteExtraLinesByTerm(termIds); + int n = payProductTermMapper.deleteByTermIds(termIds); + payProductTermMapper.restorePrematureClosed(priceSetId, today(), operator); + payProductTermMapper.clearScheduledEnd(priceSetId, operator); + return n; + } + + @Override + public List listLabelChannels(Long priceSetId) + { + if (priceSetId == null) + { + return Collections.emptyList(); + } + List list = channelProductMapMapper.selectByPriceSetId(priceSetId); + return list == null ? Collections.emptyList() : list; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String bindLabelChannels(Long priceSetId, List channelIds, Long productId) + { + if (priceSetId == null) + { + throw new ServiceException("请选择打单套餐"); + } + if (channelIds == null || channelIds.isEmpty()) + { + throw new ServiceException("请选择要绑定的渠道"); + } + if (productId == null) + { + throw new ServiceException("请选择套餐内应收产品"); + } + PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId); + if (set == null || !PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(set.getSetCategory()))) + { + throw new ServiceException("打单套餐不存在"); + } + Long root = resolveRoot(productId); + boolean inSet = false; + List products = selectLabelProducts(priceSetId); + for (PayProduct p : products) + { + if (p != null && root.equals(p.getProductId())) + { + inSet = true; + break; + } + } + if (!inSet) + { + throw new ServiceException("所选产品不属于本打单套餐"); + } + String operator = SecurityUtils.getUsername(); + int n = 0; + for (String channelId : channelIds) + { + if (isBlank(channelId)) + { + continue; + } + ServiceChannel channel = serviceChannelMapper.selectServiceChannelById(channelId.trim()); + if (channel == null || isBlank(channel.getChannelCode())) + { + throw new ServiceException("渠道不存在或渠道编码为空:" + channelId); + } + ChannelProductMap row = new ChannelProductMap(); + row.setChannelId(channelId.trim()); + row.setChannelCode(channel.getChannelCode()); + row.setProductId(root); + row.setPriceSetId(priceSetId); + row.setPayType(PAY_TYPE_AR); + row.setCreateBy(operator); + row.setUpdateBy(operator); + channelProductMapMapper.upsert(row); + n++; + } + if (n <= 0) + { + throw new ServiceException("没有可绑定的渠道"); + } + return "已绑定 " + n + " 个渠道"; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int unbindLabelChannel(Long priceSetId, String channelId) + { + if (priceSetId == null || isBlank(channelId)) + { + return 0; + } + return channelProductMapMapper.deleteByChannelTypeAndPriceSet(channelId.trim(), PAY_TYPE_AR, priceSetId); + } + + private Long resolveLabelPriceSetId(Long productId) + { + if (productId == null) + { + return null; + } + return payProductTermMapper.selectLabelPriceSetIdByProduct(productId); + } + @Override public List selectLabelProducts(Long priceSetId) { @@ -1196,9 +1336,9 @@ public class PayProductServiceImpl implements IPayProductService { return Collections.emptyList(); } - Date now = today(); + Date now = nowDateTime(); // 纠正历史:未来生效版启动时把旧版提前标成已失效 - payProductTermMapper.restorePrematureClosed(priceSetId, now, "system"); + payProductTermMapper.restorePrematureClosed(priceSetId, today(), "system"); List groups = payProductTermMapper.selectLabelVersionGroups(priceSetId); if (groups == null || groups.isEmpty()) { @@ -1212,16 +1352,16 @@ public class PayProductServiceImpl implements IPayProductService 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))) + else if (toLocalDateTime(start).isAfter(toLocalDateTime(now))) { ver.setStatus(TERM_PENDING); } - else if (end != null && toLocalDate(end).isBefore(toLocalDate(now))) + else if (end != null && !toLocalDateTime(end).isAfter(toLocalDateTime(now))) { ver.setStatus(TERM_EXPIRED); } @@ -1255,20 +1395,22 @@ public class PayProductServiceImpl implements IPayProductService throw new ServiceException("只能启动打单套餐版本"); } String operator = SecurityUtils.getUsername(); - boolean future = toLocalDate(effectiveTime).isAfter(toLocalDate(today())); - Date endDate = toDate(toLocalDate(effectiveTime).minusDays(1)); + Date effectiveHour = truncateToHour(effectiveTime); + boolean future = toLocalDateTime(effectiveHour).isAfter(toLocalDateTime(nowDateTime())); + // 旧版失效时间 = 新版生效时间(取价按 start <= biz < end) + Date endDate = effectiveHour; 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); + int n = payProductTermMapper.updateLabelVersionPeriod(priceSetId, versionNo, effectiveHour, null, termStatus, operator); if (n <= 0) { throw new ServiceException("该版本没有可启动的产品价卡"); @@ -1611,12 +1753,52 @@ public class PayProductServiceImpl implements IPayProductService row.setChannelCode(channel.getChannelCode()); row.setProductId(storedId); row.setPayType(type); + if (PAY_TYPE_AR.equals(type)) + { + row.setPriceSetId(resolveLabelPriceSetId(storedId)); + } String operator = SecurityUtils.getUsername(); row.setCreateBy(operator); row.setUpdateBy(operator); channelProductMapMapper.upsert(row); } + @Override + @Transactional(rollbackFor = Exception.class) + public String batchBindChannel(List channelIds, Long apProductId, Long arProductId) + { + if (channelIds == null || channelIds.isEmpty()) + { + throw new ServiceException("请先勾选渠道"); + } + if (apProductId == null && arProductId == null) + { + throw new ServiceException("请至少选择应付或应收产品"); + } + int n = 0; + for (String channelId : channelIds) + { + if (isBlank(channelId)) + { + continue; + } + if (apProductId != null) + { + bindChannel(channelId.trim(), apProductId, PAY_TYPE_AP); + } + if (arProductId != null) + { + bindChannel(channelId.trim(), arProductId, PAY_TYPE_AR); + } + n++; + } + if (n <= 0) + { + throw new ServiceException("没有可绑定的渠道"); + } + return "已为 " + n + " 个渠道绑定产品"; + } + @Override public void unbindChannel(String channelId, String payType) { @@ -1971,6 +2153,12 @@ public class PayProductServiceImpl implements IPayProductService return toDate(java.time.LocalDate.now(java.time.ZoneId.of("GMT+8"))); } + private static Date nowDateTime() + { + return java.util.Date.from(java.time.LocalDateTime.now(java.time.ZoneId.of("GMT+8")) + .atZone(java.time.ZoneId.of("GMT+8")).toInstant()); + } + private static Date toDate(java.time.LocalDate day) { return java.sql.Date.valueOf(day); @@ -1978,11 +2166,30 @@ public class PayProductServiceImpl implements IPayProductService private static java.time.LocalDate toLocalDate(Date date) { + return toLocalDateTime(date).toLocalDate(); + } + + private static java.time.LocalDateTime toLocalDateTime(Date date) + { + if (date == null) + { + return null; + } if (date instanceof java.sql.Date sql) { - return sql.toLocalDate(); + return sql.toLocalDate().atStartOfDay(); } - return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDate(); + if (date instanceof java.sql.Timestamp ts) + { + return ts.toLocalDateTime(); + } + return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDateTime(); + } + + private static Date truncateToHour(Date date) + { + java.time.LocalDateTime dt = toLocalDateTime(date).withMinute(0).withSecond(0).withNano(0); + return java.util.Date.from(dt.atZone(java.time.ZoneId.of("GMT+8")).toInstant()); } private static String formatDay(Date date) 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 2d5157fd..30e48929 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,22 +6,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + insert into srv_channel_product_map ( - channel_id, channel_code, product_id, pay_type, create_by, create_time, update_by, update_time + channel_id, channel_code, product_id, price_set_id, pay_type, create_by, create_time, update_by, update_time ) values ( - #{channelId}, #{channelCode}, #{productId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate() + #{channelId}, #{channelCode}, #{productId}, #{priceSetId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate() ) on duplicate key update channel_code = values(channel_code), product_id = values(product_id), + price_set_id = values(price_set_id), update_by = values(update_by), update_time = sysdate() @@ -31,6 +46,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where channel_id = #{channelId} and pay_type = #{payType} + + delete from srv_channel_product_map + where channel_id = #{channelId} + and pay_type = #{payType} + and price_set_id = #{priceSetId} + + delete from srv_channel_product_map where product_id = #{productId} diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayProductMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayProductMapper.xml index d353789f..e9b88077 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PayProductMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayProductMapper.xml @@ -274,9 +274,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" limit 1 + diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayProductTermMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayProductTermMapper.xml index b955847b..8d9a6563 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PayProductTermMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayProductTermMapper.xml @@ -41,6 +41,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" limit 1 + + @@ -236,6 +245,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and effective_end >= #{today} + + update pay_product_term + set effective_end = null, + updated_by = #{updateBy}, + update_time = sysdate() + where price_set_id = #{priceSetId} + and status = '1' + and effective_end is not null + + delete from pay_product_term where product_id in #{id} diff --git a/bms-front/src/api/price/label.js b/bms-front/src/api/price/label.js index 688699f0..d1a6dba2 100644 --- a/bms-front/src/api/price/label.js +++ b/bms-front/src/api/price/label.js @@ -93,6 +93,13 @@ export function activateLabelVersion(priceSetId, versionNo, data) { }) } +export function deleteLabelVersion(priceSetId, versionNo) { + return request({ + url: '/price/label/' + priceSetId + '/versions/' + versionNo, + method: 'delete' + }) +} + export function listLabelProducts(priceSetId, versionNo) { return request({ url: '/price/label/' + priceSetId + '/products', @@ -154,3 +161,25 @@ export function removeLabelProducts(priceSetId, productIds) { method: 'delete' }) } + +export function listLabelChannels(priceSetId) { + return request({ + url: '/price/label/' + priceSetId + '/channels', + method: 'get' + }) +} + +export function bindLabelChannels(priceSetId, data) { + return request({ + url: '/price/label/' + priceSetId + '/channels', + method: 'put', + data + }) +} + +export function unbindLabelChannel(priceSetId, channelId) { + return request({ + url: '/price/label/' + priceSetId + '/channels/' + channelId, + method: 'delete' + }) +} diff --git a/bms-front/src/api/price/pay.js b/bms-front/src/api/price/pay.js index 25277a19..fbdb60e5 100644 --- a/bms-front/src/api/price/pay.js +++ b/bms-front/src/api/price/pay.js @@ -51,7 +51,9 @@ export function updatePay(data) { return request({ url: '/price/pay', method: 'put', - data + data, + headers: { repeatSubmit: false }, + timeout: 120000 }) } @@ -72,6 +74,15 @@ export function bindChannelPay(channelId, productId, payType = 'AP') { }) } +/** 批量绑定渠道应收/应付产品;至少传一个产品 */ +export function batchBindChannelPay(data) { + return request({ + url: '/price/pay/channel/batchBind', + method: 'put', + data + }) +} + export function comparePay(leftId, rightId) { return request({ url: '/price/pay/compare', diff --git a/bms-front/src/views/data/serviceChannel/index.vue b/bms-front/src/views/data/serviceChannel/index.vue index 13290b60..a497dcec 100644 --- a/bms-front/src/views/data/serviceChannel/index.vue +++ b/bms-front/src/views/data/serviceChannel/index.vue @@ -21,6 +21,16 @@ 导出 + + 批量绑定产品 + + + + + {{ batchBindIds.length }} 个 + + + + + + + + + + + +
至少选择一个产品;将覆盖已选渠道对应收付类型的绑定。
+
+ +
+ @@ -174,7 +207,7 @@ import { listServiceChannel, getServiceChannel, addServiceChannel, updateServiceChannel, delServiceChannel, changeServiceChannelStatus } from '@/api/masterdata/serviceChannel' -import { payOptions as listPayOptions, getChannelPay, bindChannelPay } from '@/api/price/pay' +import { payOptions as listPayOptions, getChannelPay, bindChannelPay, batchBindChannelPay } from '@/api/price/pay' const { proxy } = getCurrentInstance() const tableRef = ref() @@ -190,6 +223,11 @@ const payProduct = ref(null) const arProduct = ref(null) const syncingPay = ref(false) const syncingAr = ref(false) +const batchBindOpen = ref(false) +const batchBindSaving = ref(false) +const batchBindIds = ref([]) +const batchApId = ref() +const batchArId = ref() const payTip = computed(() => { if (viewing.value) { if (payProduct.value?.versionNo) return `已绑定第 ${payProduct.value.versionNo} 版。` @@ -293,8 +331,7 @@ function resetPay() { } function payOptionLabel(item) { - const version = item.versionNo > 1 ? `(当前第 ${item.versionNo} 版)` : '' - return `${item.productName}${version}` + return item?.productName || '' } function loadPayOptions() { @@ -447,6 +484,46 @@ function handleExport() { ...tableRef.value.getQueryParams() }, `service_channel_${new Date().getTime()}.xlsx`) } + +function openBatchBind(selection, ids) { + const list = Array.isArray(ids) ? ids.filter(Boolean) : [] + if (!list.length) { + proxy.$modal.msgWarning('请先勾选渠道') + return + } + batchBindIds.value = list.map(String) + batchApId.value = undefined + batchArId.value = undefined + loadPayOptions() + batchBindOpen.value = true +} + +function onBatchBindClosed() { + batchBindIds.value = [] + batchApId.value = undefined + batchArId.value = undefined + batchBindSaving.value = false +} + +function submitBatchBind() { + if (!batchBindIds.value.length) return + if (!batchApId.value && !batchArId.value) { + proxy.$modal.msgWarning('请至少选择应付或应收产品') + return + } + batchBindSaving.value = true + batchBindChannelPay({ + channelIds: batchBindIds.value, + apProductId: batchApId.value || null, + arProductId: batchArId.value || null + }).then(res => { + proxy.$modal.msgSuccess(res.msg || res.data || '批量绑定成功') + batchBindOpen.value = false + tableRef.value?.refresh() + }).finally(() => { + batchBindSaving.value = false + }) +} diff --git a/bms-front/src/views/price/label/components/LabelVersionProductDrawer.vue b/bms-front/src/views/price/label/components/LabelVersionProductDrawer.vue index a8c98a9f..c36f588b 100644 --- a/bms-front/src/views/price/label/components/LabelVersionProductDrawer.vue +++ b/bms-front/src/views/price/label/components/LabelVersionProductDrawer.vue @@ -95,11 +95,19 @@ const filteredRows = computed(() => { }) }) +function formatHour(val) { + if (!val) return '' + const text = String(val).replace('T', ' ') + if (text.length >= 16) return text.slice(0, 13) + ':00' + if (text.length >= 10) return text.slice(0, 10) + ' 00:00' + return text +} + function termText(row) { - const start = row.termStart || row.effectiveDate || '' - const end = row.termEnd || '不限' + const start = formatHour(row.termStart || row.effectiveDate || '') + const end = row.termEnd ? formatHour(row.termEnd) : '不限' if (!start) return '未设置' - return `${String(start).slice(0, 10)} ~ ${String(end).slice(0, 10)}` + return start + ' ~ ' + end } function termStatusText(status) { diff --git a/bms-front/src/views/price/label/components/LabelVersionTable.vue b/bms-front/src/views/price/label/components/LabelVersionTable.vue index 76f74ac7..f7b007a9 100644 --- a/bms-front/src/views/price/label/components/LabelVersionTable.vue +++ b/bms-front/src/views/price/label/components/LabelVersionTable.vue @@ -28,15 +28,15 @@ - + - - + + @@ -111,9 +111,9 @@ const importMeta = reactive({ overwrite: false }) const detailVisible = ref(false) -const detailEditing = ref(false) const detailProduct = ref(null) const detailRowKey = ref('') +const savingRowKey = ref('') const detailTitle = computed(() => detailProduct.value?.productName || detailProduct.value?.sheetName || '产品明细') const priceSetId = computed(() => { @@ -135,13 +135,13 @@ const cardTitle = computed(() => { const pageBanner = computed(() => { const name = form.basic.priceSetName || '' const code = form.basic.priceSetCode || '' - if (editVersionMode.value) return `编辑待生效版本 V${versionNo.value}:${name}(${code})` + if (editVersionMode.value) return `编辑版本 V${versionNo.value}:${name}(${code})` return `新建版本:${name}(${code})` }) const headerTip = computed(() => { - if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本待生效版本,不新增版本号' + if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本版本,不新增版本号' if (newVersionMode.value) return '导入后生成待生效新版本;生效时间请回详情页设置' - return '打单价卡与应付独立;解析后回填到此表,查看后点修改可改明细' + return '打单价卡与应付独立;解析后回填到此表,查看明细后可在行内保存修改' }) function goBack() { @@ -219,28 +219,21 @@ function onImportConfirm(payload) { function openDetail(row) { if (!row) return - detailEditing.value = false + savingRowKey.value = '' detailRowKey.value = row.rowKey || `${row.sheetName || ''}-${row.productName || ''}` detailProduct.value = JSON.parse(JSON.stringify(row)) detailVisible.value = true } -function startEdit() { - detailEditing.value = true -} - function onDetailClosed() { - detailEditing.value = false detailProduct.value = null detailRowKey.value = '' + savingRowKey.value = '' } -function applyDetail() { +function applyEditedProduct() { const edited = detailProduct.value - if (!edited) { - detailVisible.value = false - return - } + if (!edited) return false const key = detailRowKey.value const idx = productRows.value.findIndex(item => { const itemKey = item.rowKey || `${item.sheetName || ''}-${item.productName || ''}` @@ -248,7 +241,7 @@ function applyDetail() { }) if (idx < 0) { proxy.$modal.msgError('未找到对应产品行') - return + return false } const bands = edited.weightBands || [] const fees = edited.surcharges || [] @@ -261,9 +254,25 @@ function applyDetail() { extraCount: extras.length, parseStatus: bands.length + fees.length + extras.length === 0 ? 'EMPTY' : (edited.parseStatus || 'OK') }) - detailEditing.value = false - detailVisible.value = false - proxy.$modal.msgSuccess('已更新该产品明细') + return true +} + +function saveDetailRow(payload) { + if (savingRowKey.value) { + payload?.done?.(false) + return + } + savingRowKey.value = payload?.rowKey || 'row' + let ok = false + try { + ok = !!applyEditedProduct() + if (ok) proxy.$modal.msgSuccess('该行已保存') + } catch (e) { + ok = false + } finally { + savingRowKey.value = '' + payload?.done?.(ok) + } } function statusType(status) { @@ -295,7 +304,7 @@ async function finish() { return } const tip = editVersionMode.value - ? `将覆盖待生效版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?` + ? `将覆盖版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?` : newVersionMode.value ? `将为套餐「${form.basic.priceSetName || ''}」新建待生效版本并保存 ${productRows.value.length} 个产品,确认继续?` : `将创建打单套餐并保存 ${productRows.value.length} 个应收产品,确认继续?` diff --git a/bms-front/src/views/price/label/detail.vue b/bms-front/src/views/price/label/detail.vue index feda1b2c..6bdb2bf5 100644 --- a/bms-front/src/views/price/label/detail.vue +++ b/bms-front/src/views/price/label/detail.vue @@ -14,10 +14,13 @@ + + @@ -45,13 +48,14 @@ -
仅待生效版本可修改;未到生效日仍为待生效,到日后自动按生效处理。
+
仅待生效版本可修改;精确到小时。未到生效时间仍为待生效,到时自动按生效处理。