From 3a3db6512924e9fcda8ef2cd4ae52e49b2c70bae Mon Sep 17 00:00:00 2001 From: chenyuan Date: Tue, 11 Aug 2026 17:59:27 +0800 Subject: [PATCH] feat: validate target user rfm insights --- .../stu/StudentTrainingAnswerController.java | 17 +++++ ...UserProfileStepThreeValidationRequest.java | 20 +++++ ...tUserProfileStepThreeValidationResult.java | 11 +++ .../TargetUserProfileStepThreeService.java | 9 +++ ...TargetUserProfileStepThreeServiceImpl.java | 63 ++++++++++++++++ ...etUserProfileStepThreeServiceImplTest.java | 74 +++++++++++++++++++ 6 files changed, 194 insertions(+) create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/TargetUserProfileStepThreeValidationRequest.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/TargetUserProfileStepThreeValidationResult.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/TargetUserProfileStepThreeService.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/impl/TargetUserProfileStepThreeServiceImpl.java create mode 100644 src/test/java/com/sztzjy/linkCommerce/service/impl/TargetUserProfileStepThreeServiceImplTest.java diff --git a/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java b/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java index 3a7010b..093b733 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java @@ -32,6 +32,8 @@ import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepOneValidationRequ import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepOneValidationResult; import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepTwoValidationRequest; import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepTwoValidationResult; +import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepThreeValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepThreeValidationResult; import com.sztzjy.linkCommerce.service.NewProductSurveyStepOneService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepOneService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepTwoService; @@ -46,6 +48,7 @@ import com.sztzjy.linkCommerce.service.ProductDevelopmentFactorsStepThreeService import com.sztzjy.linkCommerce.service.ProductDevelopmentFactorsStepFourService; import com.sztzjy.linkCommerce.service.TargetUserProfileStepOneService; import com.sztzjy.linkCommerce.service.TargetUserProfileStepTwoService; +import com.sztzjy.linkCommerce.service.TargetUserProfileStepThreeService; import com.sztzjy.linkCommerce.service.StudentTrainingAnswerService; import com.sztzjy.linkCommerce.util.ResultEntity; import io.swagger.annotations.Api; @@ -103,6 +106,9 @@ public class StudentTrainingAnswerController { @Autowired private TargetUserProfileStepTwoService targetUserProfileStepTwoService; + @Autowired + private TargetUserProfileStepThreeService targetUserProfileStepThreeService; + @GetMapping("/{taskKey}") @ApiOperation("查询当前学生指定实训任务答案") public ResultEntity detail(@PathVariable String taskKey, @@ -285,6 +291,17 @@ public class StudentTrainingAnswerController { } } + @PostMapping("/target-user-profile/step-3/validate") + @ApiOperation("目标用户画像第三步校验") + public ResultEntity validateTargetUserProfileStepThree( + @RequestBody TargetUserProfileStepThreeValidationRequest answer, HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "校验通过", targetUserProfileStepThreeService.validate(answer, TokenProvider.getJWTUser(request))); + } catch (ServiceException e) { + return new ResultEntity<>(e.getCode(), e.getMessage()); + } + } + @DeleteMapping("/{taskKey}") @ApiOperation("删除当前学生指定实训任务答案") public ResultEntity delete(@PathVariable String taskKey, diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/TargetUserProfileStepThreeValidationRequest.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/TargetUserProfileStepThreeValidationRequest.java new file mode 100644 index 0000000..4d03eee --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/TargetUserProfileStepThreeValidationRequest.java @@ -0,0 +1,20 @@ +package com.sztzjy.linkCommerce.entity.dto; + +import java.util.List; + +public class TargetUserProfileStepThreeValidationRequest { + private List insights; + + public List getInsights() { return insights; } + public void setInsights(List insights) { this.insights = insights; } + + public static class Insight { + private String dimension; + private String analysis; + + public String getDimension() { return dimension; } + public void setDimension(String dimension) { this.dimension = dimension; } + public String getAnalysis() { return analysis; } + public void setAnalysis(String analysis) { this.analysis = analysis; } + } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/TargetUserProfileStepThreeValidationResult.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/TargetUserProfileStepThreeValidationResult.java new file mode 100644 index 0000000..6344fad --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/TargetUserProfileStepThreeValidationResult.java @@ -0,0 +1,11 @@ +package com.sztzjy.linkCommerce.entity.dto; + +public class TargetUserProfileStepThreeValidationResult { + private boolean valid; + private String message; + + public boolean isValid() { return valid; } + public void setValid(boolean valid) { this.valid = valid; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/TargetUserProfileStepThreeService.java b/src/main/java/com/sztzjy/linkCommerce/service/TargetUserProfileStepThreeService.java new file mode 100644 index 0000000..426d019 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/TargetUserProfileStepThreeService.java @@ -0,0 +1,9 @@ +package com.sztzjy.linkCommerce.service; + +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepThreeValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepThreeValidationResult; + +public interface TargetUserProfileStepThreeService { + TargetUserProfileStepThreeValidationResult validate(TargetUserProfileStepThreeValidationRequest request, JwtUser user); +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/TargetUserProfileStepThreeServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/TargetUserProfileStepThreeServiceImpl.java new file mode 100644 index 0000000..f94547c --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/TargetUserProfileStepThreeServiceImpl.java @@ -0,0 +1,63 @@ +package com.sztzjy.linkCommerce.service.impl; + +import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer; +import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepThreeValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepThreeValidationResult; +import com.sztzjy.linkCommerce.service.StudentTrainingAnswerService; +import com.sztzjy.linkCommerce.service.TargetUserProfileStepThreeService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +@Service +public class TargetUserProfileStepThreeServiceImpl implements TargetUserProfileStepThreeService { + private static final String TASK_KEY = "target-user-profile"; + private static final Set REQUIRED_DIMENSIONS = new LinkedHashSet<>(Arrays.asList("高价值用户占比", "用户活跃度", "消费能力", "用户忠诚度", "重点客户群特征")); + private static final Set REQUIRED_CHART_TYPES = new LinkedHashSet<>(Arrays.asList("segment-share", "average-spend", "score-heatmap")); + + @Autowired + private StudentTrainingAnswerService studentTrainingAnswerService; + + @Override + public TargetUserProfileStepThreeValidationResult validate(TargetUserProfileStepThreeValidationRequest request, JwtUser user) { + if (user == null || StringUtils.isBlank(user.getUserId())) { + throw new ServiceException(HttpStatus.UNAUTHORIZED, "请先登录后再校验"); + } + if (user.getRoleId() != 4) { + throw new ServiceException(HttpStatus.FORBIDDEN, "仅学生可进行实训校验"); + } + StudentTrainingAnswer savedAnswer = studentTrainingAnswerService.get(TASK_KEY, null, user); + if (savedAnswer == null || StringUtils.isBlank(savedAnswer.getStep1Answer())) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并保存第 2 步 RFM 图表"); + } + String savedContent = savedAnswer.getStep1Answer(); + if (REQUIRED_CHART_TYPES.stream().anyMatch(type -> !StringUtils.contains(savedContent, type))) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完整保存第 2 步的三类 RFM 图表"); + } + if (request == null || request.getInsights() == null || request.getInsights().size() != REQUIRED_DIMENSIONS.size()) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "请完成五个 RFM 用户群体特点分析维度"); + } + Set dimensions = new LinkedHashSet<>(); + for (TargetUserProfileStepThreeValidationRequest.Insight insight : request.getInsights()) { + String dimension = insight == null ? "" : StringUtils.trimToEmpty(insight.getDimension()); + String analysis = insight == null ? "" : StringUtils.trimToEmpty(insight.getAnalysis()); + if (!REQUIRED_DIMENSIONS.contains(dimension) || !dimensions.add(dimension) || StringUtils.isBlank(analysis)) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "请完成五个 RFM 用户群体特点分析维度"); + } + } + if (!dimensions.equals(REQUIRED_DIMENSIONS)) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "提交的分析维度不完整"); + } + TargetUserProfileStepThreeValidationResult result = new TargetUserProfileStepThreeValidationResult(); + result.setValid(true); + result.setMessage("校验通过"); + return result; + } +} diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/TargetUserProfileStepThreeServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/TargetUserProfileStepThreeServiceImplTest.java new file mode 100644 index 0000000..86eed67 --- /dev/null +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/TargetUserProfileStepThreeServiceImplTest.java @@ -0,0 +1,74 @@ +package com.sztzjy.linkCommerce.service.impl; + +import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer; +import com.sztzjy.linkCommerce.entity.dto.TargetUserProfileStepThreeValidationRequest; +import com.sztzjy.linkCommerce.service.StudentTrainingAnswerService; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TargetUserProfileStepThreeServiceImplTest { + @Test + void validatesAllInsightsAfterChartsAreSaved() { + assertTrue(serviceWithCharts().validate(request(), student()).isValid()); + } + + @Test + void rejectsMissingInsightAndMissingChartHistory() { + TargetUserProfileStepThreeValidationRequest incomplete = request(); + incomplete.setInsights(new ArrayList<>(incomplete.getInsights())); + incomplete.getInsights().remove(4); + assertThrows(ServiceException.class, () -> serviceWithCharts().validate(incomplete, student())); + + TargetUserProfileStepThreeServiceImpl service = new TargetUserProfileStepThreeServiceImpl(); + StudentTrainingAnswerService answers = mock(StudentTrainingAnswerService.class); + StudentTrainingAnswer answer = new StudentTrainingAnswer(); + answer.setStep1Answer("{\"rfmCharts\":{\"segment-share\":{}}}"); + when(answers.get(anyString(), any(), any(JwtUser.class))).thenReturn(answer); + ReflectionTestUtils.setField(service, "studentTrainingAnswerService", answers); + assertThrows(ServiceException.class, () -> service.validate(request(), student())); + } + + private TargetUserProfileStepThreeServiceImpl serviceWithCharts() { + TargetUserProfileStepThreeServiceImpl service = new TargetUserProfileStepThreeServiceImpl(); + StudentTrainingAnswerService answers = mock(StudentTrainingAnswerService.class); + StudentTrainingAnswer answer = new StudentTrainingAnswer(); + answer.setStep1Answer("{\"rfmCharts\":{\"segment-share\":{},\"average-spend\":{},\"score-heatmap\":{}}}"); + when(answers.get(anyString(), any(), any(JwtUser.class))).thenReturn(answer); + ReflectionTestUtils.setField(service, "studentTrainingAnswerService", answers); + return service; + } + + private TargetUserProfileStepThreeValidationRequest request() { + TargetUserProfileStepThreeValidationRequest request = new TargetUserProfileStepThreeValidationRequest(); + request.setInsights(Arrays.asList( + insight("高价值用户占比"), insight("用户活跃度"), insight("消费能力"), insight("用户忠诚度"), insight("重点客户群特征") + )); + return request; + } + + private TargetUserProfileStepThreeValidationRequest.Insight insight(String dimension) { + TargetUserProfileStepThreeValidationRequest.Insight insight = new TargetUserProfileStepThreeValidationRequest.Insight(); + insight.setDimension(dimension); + insight.setAnalysis("已完成" + dimension + "分析"); + return insight; + } + + private JwtUser student() { + JwtUser user = new JwtUser(); + user.setUserId("stu-1"); + user.setRoleId(4); + return user; + } +}