From df87a052f3e81137bac5a951f9f272b7de1e93e3 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Wed, 2 Sep 2026 20:21:22 +0800 Subject: [PATCH] fix: validate dynamic diagnosis channels --- ...osisIterationStepOneValidationRequest.java | 20 ++++++ ...tDiagnosisIterationStepOneServiceImpl.java | 65 ++++++++++++++----- ...gnosisIterationStepOneServiceImplTest.java | 25 +++++++ 3 files changed, 93 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDiagnosisIterationStepOneValidationRequest.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDiagnosisIterationStepOneValidationRequest.java index 22e29ff..c005fc4 100644 --- a/src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDiagnosisIterationStepOneValidationRequest.java +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDiagnosisIterationStepOneValidationRequest.java @@ -2,12 +2,16 @@ package com.sztzjy.linkCommerce.entity.dto; import java.math.BigDecimal; import java.util.List; +import java.util.Map; public class ProductDiagnosisIterationStepOneValidationRequest { + private List channels; private List conversionRates; private String funnelChartUrl; private List anomalyRows; + public List getChannels() { return channels; } + public void setChannels(List channels) { this.channels = channels; } public List getConversionRates() { return conversionRates; } public void setConversionRates(List conversionRates) { this.conversionRates = conversionRates; } public String getFunnelChartUrl() { return funnelChartUrl; } @@ -15,8 +19,19 @@ public class ProductDiagnosisIterationStepOneValidationRequest { public List getAnomalyRows() { return anomalyRows; } public void setAnomalyRows(List anomalyRows) { this.anomalyRows = anomalyRows; } + public static class Channel { + private String id; + private String name; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + public static class ConversionRate { private String linkNode; + private Map channelRates; private BigDecimal tmall; private BigDecimal jd; private BigDecimal douyin; @@ -25,6 +40,8 @@ public class ProductDiagnosisIterationStepOneValidationRequest { public String getLinkNode() { return linkNode; } public void setLinkNode(String linkNode) { this.linkNode = linkNode; } + public Map getChannelRates() { return channelRates; } + public void setChannelRates(Map channelRates) { this.channelRates = channelRates; } public BigDecimal getTmall() { return tmall; } public void setTmall(BigDecimal tmall) { this.tmall = tmall; } public BigDecimal getJd() { return jd; } @@ -38,12 +55,15 @@ public class ProductDiagnosisIterationStepOneValidationRequest { } public static class AnomalyRow { + private String channelId; private String channel; private String linkNode; private BigDecimal storeConversionRate; private BigDecimal industryAverageConversionRate; private String anomalyAnalysis; + public String getChannelId() { return channelId; } + public void setChannelId(String channelId) { this.channelId = channelId; } public String getChannel() { return channel; } public void setChannel(String channel) { this.channel = channel; } public String getLinkNode() { return linkNode; } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/ProductDiagnosisIterationStepOneServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/ProductDiagnosisIterationStepOneServiceImpl.java index a2b78f5..f3d2544 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/ProductDiagnosisIterationStepOneServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/ProductDiagnosisIterationStepOneServiceImpl.java @@ -18,15 +18,15 @@ import java.util.Map; @Service public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagnosisIterationStepOneService { private static final List LINK_NODES = Arrays.asList("曝光-点击", "点击-加购", "加购-下单", "下单-好评", "好评-复购"); - private static final List CHANNELS = Arrays.asList("天猫", "京东", "抖音", "拼多多"); @Override public ProductDiagnosisIterationStepOneValidationResult validate(ProductDiagnosisIterationStepOneValidationRequest request, JwtUser user) { requireStudent(user); if (request == null) throw badRequest("请完成全链路转化数据填写"); - Map rates = validateConversionRates(request.getConversionRates()); + Map channels = validateChannels(request.getChannels()); + Map rates = validateConversionRates(request.getConversionRates(), channels); if (StringUtils.isBlank(request.getFunnelChartUrl())) throw badRequest("请上传节点转化漏斗对比图"); - validateAnomalyRows(request.getAnomalyRows(), rates); + validateAnomalyRows(request.getAnomalyRows(), rates, channels); ProductDiagnosisIterationStepOneValidationResult result = new ProductDiagnosisIterationStepOneValidationResult(); result.setValid(true); @@ -34,17 +34,36 @@ public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagn return result; } + private Map validateChannels(List rows) { + Map channels = new LinkedHashMap<>(); + if (rows == null) { + channels.put("tmall", "天猫"); + channels.put("jd", "京东"); + channels.put("douyin", "抖音"); + channels.put("pinduoduo", "拼多多"); + return channels; + } + if (rows.isEmpty()) throw badRequest("请至少添加 1 个渠道"); + for (ProductDiagnosisIterationStepOneValidationRequest.Channel row : rows) { + String id = row == null ? "" : StringUtils.trimToEmpty(row.getId()); + String name = row == null ? "" : StringUtils.trimToEmpty(row.getName()); + if (StringUtils.isBlank(id) || StringUtils.isBlank(name)) throw badRequest("渠道名称不能为空"); + if (channels.containsKey(id) || channels.containsValue(name)) throw badRequest("渠道名称不能重复"); + channels.put(id, name); + } + return channels; + } + private Map validateConversionRates( - List rows) { + List rows, Map channels) { if (rows == null || rows.size() != LINK_NODES.size()) throw badRequest("请完整填写 5 个链路节点的转化率"); Map byNode = new LinkedHashMap<>(); for (ProductDiagnosisIterationStepOneValidationRequest.ConversionRate row : rows) { String node = row == null ? "" : StringUtils.trimToEmpty(row.getLinkNode()); if (!LINK_NODES.contains(node) || byNode.put(node, row) != null) throw badRequest("链路节点必须与题目中的 5 个节点一致且不能重复"); - validateRate(row.getTmall(), node + "的天猫转化率"); - validateRate(row.getJd(), node + "的京东转化率"); - validateRate(row.getDouyin(), node + "的抖音转化率"); - validateRate(row.getPinduoduo(), node + "的拼多多转化率"); + for (Map.Entry channel : channels.entrySet()) { + validateRate(rateForChannel(row, channel.getKey()), node + "的" + channel.getValue() + "转化率"); + } validateRate(row.getIndustryAverage(), node + "的行业均值转化率"); } if (byNode.size() != LINK_NODES.size()) throw badRequest("请完整填写 5 个链路节点的转化率"); @@ -52,18 +71,20 @@ public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagn } private void validateAnomalyRows(List rows, - Map rates) { + Map rates, + Map channels) { if (rows == null || rows.isEmpty() || rows.size() > 5) throw badRequest("异常节点分析最少填写 1 行,最多填写 5 行"); for (int index = 0; index < rows.size(); index++) { ProductDiagnosisIterationStepOneValidationRequest.AnomalyRow row = rows.get(index); if (row == null) throw badRequest("请完整填写第 " + (index + 1) + " 行异常节点分析"); - String channel = row == null ? "" : StringUtils.trimToEmpty(row.getChannel()); + String channelId = StringUtils.trimToEmpty(row.getChannelId()); + if (StringUtils.isBlank(channelId)) channelId = findChannelId(channels, row.getChannel()); String node = row == null ? "" : StringUtils.trimToEmpty(row.getLinkNode()); - if (!CHANNELS.contains(channel)) throw badRequest("第 " + (index + 1) + " 行渠道必须来源于第一个表格"); + if (!channels.containsKey(channelId)) throw badRequest("第 " + (index + 1) + " 行渠道必须来源于第一个表格"); ProductDiagnosisIterationStepOneValidationRequest.ConversionRate source = rates.get(node); if (source == null) throw badRequest("第 " + (index + 1) + " 行链路节点必须来源于第一个表格"); if (row.getStoreConversionRate() == null || row.getIndustryAverageConversionRate() == null - || row.getStoreConversionRate().compareTo(rateForChannel(source, channel)) != 0 + || row.getStoreConversionRate().compareTo(rateForChannel(source, channelId)) != 0 || row.getIndustryAverageConversionRate().compareTo(source.getIndustryAverage()) != 0) { throw badRequest("第 " + (index + 1) + " 行的店铺和行业转化率必须与第一个表格对应数据一致"); } @@ -71,11 +92,21 @@ public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagn } } - private BigDecimal rateForChannel(ProductDiagnosisIterationStepOneValidationRequest.ConversionRate rate, String channel) { - if ("天猫".equals(channel)) return rate.getTmall(); - if ("京东".equals(channel)) return rate.getJd(); - if ("抖音".equals(channel)) return rate.getDouyin(); - return rate.getPinduoduo(); + private String findChannelId(Map channels, String channelName) { + String name = StringUtils.trimToEmpty(channelName); + for (Map.Entry channel : channels.entrySet()) { + if (channel.getValue().equals(name)) return channel.getKey(); + } + return ""; + } + + private BigDecimal rateForChannel(ProductDiagnosisIterationStepOneValidationRequest.ConversionRate rate, String channelId) { + if (rate.getChannelRates() != null && rate.getChannelRates().containsKey(channelId)) return rate.getChannelRates().get(channelId); + if ("tmall".equals(channelId)) return rate.getTmall(); + if ("jd".equals(channelId)) return rate.getJd(); + if ("douyin".equals(channelId)) return rate.getDouyin(); + if ("pinduoduo".equals(channelId)) return rate.getPinduoduo(); + return null; } private void validateRate(BigDecimal rate, String label) { diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/ProductDiagnosisIterationStepOneServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/ProductDiagnosisIterationStepOneServiceImplTest.java index 16569c6..ab4cc63 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/ProductDiagnosisIterationStepOneServiceImplTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/ProductDiagnosisIterationStepOneServiceImplTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -52,6 +54,29 @@ class ProductDiagnosisIterationStepOneServiceImplTest { assertThrows(ServiceException.class, () -> service.validate(noDiagnosis, student())); } + @Test + void acceptsChannelsAddedByTheStudentAndUsesTheirDynamicRates() { + ProductDiagnosisIterationStepOneServiceImpl service = new ProductDiagnosisIterationStepOneServiceImpl(); + ProductDiagnosisIterationStepOneValidationRequest request = validRequest(); + ProductDiagnosisIterationStepOneValidationRequest.Channel channel = new ProductDiagnosisIterationStepOneValidationRequest.Channel(); + channel.setId("xiaohongshu"); + channel.setName("小红书"); + request.setChannels(Collections.singletonList(channel)); + for (ProductDiagnosisIterationStepOneValidationRequest.ConversionRate rate : request.getConversionRates()) { + Map channelRates = new LinkedHashMap<>(); + channelRates.put("xiaohongshu", rate.getTmall()); + rate.setChannelRates(channelRates); + } + ProductDiagnosisIterationStepOneValidationRequest.AnomalyRow anomaly = request.getAnomalyRows().get(0); + anomaly.setChannelId("xiaohongshu"); + anomaly.setChannel("小红书"); + + assertTrue(service.validate(request, student()).isValid()); + + request.getConversionRates().get(0).getChannelRates().remove("xiaohongshu"); + assertThrows(ServiceException.class, () -> service.validate(request, student())); + } + private ProductDiagnosisIterationStepOneValidationRequest validRequest() { ProductDiagnosisIterationStepOneValidationRequest request = new ProductDiagnosisIterationStepOneValidationRequest(); request.setConversionRates(Arrays.asList(