merge: product diagnosis dynamic channels

bugfix/ai-assessment-evidence-rescore
chenyuan 1 month ago
commit 3ffdf606bf

@ -2,12 +2,16 @@ package com.sztzjy.linkCommerce.entity.dto;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map;
public class ProductDiagnosisIterationStepOneValidationRequest { public class ProductDiagnosisIterationStepOneValidationRequest {
private List<Channel> channels;
private List<ConversionRate> conversionRates; private List<ConversionRate> conversionRates;
private String funnelChartUrl; private String funnelChartUrl;
private List<AnomalyRow> anomalyRows; private List<AnomalyRow> anomalyRows;
public List<Channel> getChannels() { return channels; }
public void setChannels(List<Channel> channels) { this.channels = channels; }
public List<ConversionRate> getConversionRates() { return conversionRates; } public List<ConversionRate> getConversionRates() { return conversionRates; }
public void setConversionRates(List<ConversionRate> conversionRates) { this.conversionRates = conversionRates; } public void setConversionRates(List<ConversionRate> conversionRates) { this.conversionRates = conversionRates; }
public String getFunnelChartUrl() { return funnelChartUrl; } public String getFunnelChartUrl() { return funnelChartUrl; }
@ -15,8 +19,19 @@ public class ProductDiagnosisIterationStepOneValidationRequest {
public List<AnomalyRow> getAnomalyRows() { return anomalyRows; } public List<AnomalyRow> getAnomalyRows() { return anomalyRows; }
public void setAnomalyRows(List<AnomalyRow> anomalyRows) { this.anomalyRows = anomalyRows; } public void setAnomalyRows(List<AnomalyRow> 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 { public static class ConversionRate {
private String linkNode; private String linkNode;
private Map<String, BigDecimal> channelRates;
private BigDecimal tmall; private BigDecimal tmall;
private BigDecimal jd; private BigDecimal jd;
private BigDecimal douyin; private BigDecimal douyin;
@ -25,6 +40,8 @@ public class ProductDiagnosisIterationStepOneValidationRequest {
public String getLinkNode() { return linkNode; } public String getLinkNode() { return linkNode; }
public void setLinkNode(String linkNode) { this.linkNode = linkNode; } public void setLinkNode(String linkNode) { this.linkNode = linkNode; }
public Map<String, BigDecimal> getChannelRates() { return channelRates; }
public void setChannelRates(Map<String, BigDecimal> channelRates) { this.channelRates = channelRates; }
public BigDecimal getTmall() { return tmall; } public BigDecimal getTmall() { return tmall; }
public void setTmall(BigDecimal tmall) { this.tmall = tmall; } public void setTmall(BigDecimal tmall) { this.tmall = tmall; }
public BigDecimal getJd() { return jd; } public BigDecimal getJd() { return jd; }
@ -38,12 +55,15 @@ public class ProductDiagnosisIterationStepOneValidationRequest {
} }
public static class AnomalyRow { public static class AnomalyRow {
private String channelId;
private String channel; private String channel;
private String linkNode; private String linkNode;
private BigDecimal storeConversionRate; private BigDecimal storeConversionRate;
private BigDecimal industryAverageConversionRate; private BigDecimal industryAverageConversionRate;
private String anomalyAnalysis; private String anomalyAnalysis;
public String getChannelId() { return channelId; }
public void setChannelId(String channelId) { this.channelId = channelId; }
public String getChannel() { return channel; } public String getChannel() { return channel; }
public void setChannel(String channel) { this.channel = channel; } public void setChannel(String channel) { this.channel = channel; }
public String getLinkNode() { return linkNode; } public String getLinkNode() { return linkNode; }

@ -18,15 +18,15 @@ import java.util.Map;
@Service @Service
public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagnosisIterationStepOneService { public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagnosisIterationStepOneService {
private static final List<String> LINK_NODES = Arrays.asList("曝光-点击", "点击-加购", "加购-下单", "下单-好评", "好评-复购"); private static final List<String> LINK_NODES = Arrays.asList("曝光-点击", "点击-加购", "加购-下单", "下单-好评", "好评-复购");
private static final List<String> CHANNELS = Arrays.asList("天猫", "京东", "抖音", "拼多多");
@Override @Override
public ProductDiagnosisIterationStepOneValidationResult validate(ProductDiagnosisIterationStepOneValidationRequest request, JwtUser user) { public ProductDiagnosisIterationStepOneValidationResult validate(ProductDiagnosisIterationStepOneValidationRequest request, JwtUser user) {
requireStudent(user); requireStudent(user);
if (request == null) throw badRequest("请完成全链路转化数据填写"); if (request == null) throw badRequest("请完成全链路转化数据填写");
Map<String, ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> rates = validateConversionRates(request.getConversionRates()); Map<String, String> channels = validateChannels(request.getChannels());
Map<String, ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> rates = validateConversionRates(request.getConversionRates(), channels);
if (StringUtils.isBlank(request.getFunnelChartUrl())) throw badRequest("请上传节点转化漏斗对比图"); if (StringUtils.isBlank(request.getFunnelChartUrl())) throw badRequest("请上传节点转化漏斗对比图");
validateAnomalyRows(request.getAnomalyRows(), rates); validateAnomalyRows(request.getAnomalyRows(), rates, channels);
ProductDiagnosisIterationStepOneValidationResult result = new ProductDiagnosisIterationStepOneValidationResult(); ProductDiagnosisIterationStepOneValidationResult result = new ProductDiagnosisIterationStepOneValidationResult();
result.setValid(true); result.setValid(true);
@ -34,17 +34,36 @@ public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagn
return result; return result;
} }
private Map<String, String> validateChannels(List<ProductDiagnosisIterationStepOneValidationRequest.Channel> rows) {
Map<String, String> 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<String, ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> validateConversionRates( private Map<String, ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> validateConversionRates(
List<ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> rows) { List<ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> rows, Map<String, String> channels) {
if (rows == null || rows.size() != LINK_NODES.size()) throw badRequest("请完整填写 5 个链路节点的转化率"); if (rows == null || rows.size() != LINK_NODES.size()) throw badRequest("请完整填写 5 个链路节点的转化率");
Map<String, ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> byNode = new LinkedHashMap<>(); Map<String, ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> byNode = new LinkedHashMap<>();
for (ProductDiagnosisIterationStepOneValidationRequest.ConversionRate row : rows) { for (ProductDiagnosisIterationStepOneValidationRequest.ConversionRate row : rows) {
String node = row == null ? "" : StringUtils.trimToEmpty(row.getLinkNode()); String node = row == null ? "" : StringUtils.trimToEmpty(row.getLinkNode());
if (!LINK_NODES.contains(node) || byNode.put(node, row) != null) throw badRequest("链路节点必须与题目中的 5 个节点一致且不能重复"); if (!LINK_NODES.contains(node) || byNode.put(node, row) != null) throw badRequest("链路节点必须与题目中的 5 个节点一致且不能重复");
validateRate(row.getTmall(), node + "的天猫转化率"); for (Map.Entry<String, String> channel : channels.entrySet()) {
validateRate(row.getJd(), node + "的京东转化率"); validateRate(rateForChannel(row, channel.getKey()), node + "的" + channel.getValue() + "转化率");
validateRate(row.getDouyin(), node + "的抖音转化率"); }
validateRate(row.getPinduoduo(), node + "的拼多多转化率");
validateRate(row.getIndustryAverage(), node + "的行业均值转化率"); validateRate(row.getIndustryAverage(), node + "的行业均值转化率");
} }
if (byNode.size() != LINK_NODES.size()) throw badRequest("请完整填写 5 个链路节点的转化率"); if (byNode.size() != LINK_NODES.size()) throw badRequest("请完整填写 5 个链路节点的转化率");
@ -52,18 +71,20 @@ public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagn
} }
private void validateAnomalyRows(List<ProductDiagnosisIterationStepOneValidationRequest.AnomalyRow> rows, private void validateAnomalyRows(List<ProductDiagnosisIterationStepOneValidationRequest.AnomalyRow> rows,
Map<String, ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> rates) { Map<String, ProductDiagnosisIterationStepOneValidationRequest.ConversionRate> rates,
Map<String, String> channels) {
if (rows == null || rows.isEmpty() || rows.size() > 5) throw badRequest("异常节点分析最少填写 1 行,最多填写 5 行"); if (rows == null || rows.isEmpty() || rows.size() > 5) throw badRequest("异常节点分析最少填写 1 行,最多填写 5 行");
for (int index = 0; index < rows.size(); index++) { for (int index = 0; index < rows.size(); index++) {
ProductDiagnosisIterationStepOneValidationRequest.AnomalyRow row = rows.get(index); ProductDiagnosisIterationStepOneValidationRequest.AnomalyRow row = rows.get(index);
if (row == null) throw badRequest("请完整填写第 " + (index + 1) + " 行异常节点分析"); 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()); 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); ProductDiagnosisIterationStepOneValidationRequest.ConversionRate source = rates.get(node);
if (source == null) throw badRequest("第 " + (index + 1) + " 行链路节点必须来源于第一个表格"); if (source == null) throw badRequest("第 " + (index + 1) + " 行链路节点必须来源于第一个表格");
if (row.getStoreConversionRate() == null || row.getIndustryAverageConversionRate() == null 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) { || row.getIndustryAverageConversionRate().compareTo(source.getIndustryAverage()) != 0) {
throw badRequest("第 " + (index + 1) + " 行的店铺和行业转化率必须与第一个表格对应数据一致"); throw badRequest("第 " + (index + 1) + " 行的店铺和行业转化率必须与第一个表格对应数据一致");
} }
@ -71,11 +92,21 @@ public class ProductDiagnosisIterationStepOneServiceImpl implements ProductDiagn
} }
} }
private BigDecimal rateForChannel(ProductDiagnosisIterationStepOneValidationRequest.ConversionRate rate, String channel) { private String findChannelId(Map<String, String> channels, String channelName) {
if ("天猫".equals(channel)) return rate.getTmall(); String name = StringUtils.trimToEmpty(channelName);
if ("京东".equals(channel)) return rate.getJd(); for (Map.Entry<String, String> channel : channels.entrySet()) {
if ("抖音".equals(channel)) return rate.getDouyin(); if (channel.getValue().equals(name)) return channel.getKey();
return rate.getPinduoduo(); }
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) { private void validateRate(BigDecimal rate, String label) {

@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; 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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -52,6 +54,29 @@ class ProductDiagnosisIterationStepOneServiceImplTest {
assertThrows(ServiceException.class, () -> service.validate(noDiagnosis, student())); 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<String, BigDecimal> 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() { private ProductDiagnosisIterationStepOneValidationRequest validRequest() {
ProductDiagnosisIterationStepOneValidationRequest request = new ProductDiagnosisIterationStepOneValidationRequest(); ProductDiagnosisIterationStepOneValidationRequest request = new ProductDiagnosisIterationStepOneValidationRequest();
request.setConversionRates(Arrays.asList( request.setConversionRates(Arrays.asList(

Loading…
Cancel
Save