From fc5b3e7b96d61dfb72d36acadee1e0f2e06feab2 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Tue, 11 Aug 2026 21:48:41 +0800 Subject: [PATCH] feat: validate consumer behavior questionnaire --- .../stu/StudentTrainingAnswerController.java | 30 +++++ ...merBehaviorStepThreeValidationRequest.java | 29 +++++ ...umerBehaviorStepThreeValidationResult.java | 11 ++ .../ConsumerBehaviorWordPreviewResult.java | 8 ++ .../ConsumerBehaviorStepThreeService.java | 12 ++ .../ConsumerBehaviorStepThreeServiceImpl.java | 120 ++++++++++++++++++ ...sumerBehaviorStepThreeServiceImplTest.java | 76 +++++++++++ 7 files changed, 286 insertions(+) create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorStepThreeValidationRequest.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorStepThreeValidationResult.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorWordPreviewResult.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/ConsumerBehaviorStepThreeService.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/impl/ConsumerBehaviorStepThreeServiceImpl.java create mode 100644 src/test/java/com/sztzjy/linkCommerce/service/impl/ConsumerBehaviorStepThreeServiceImplTest.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 dc859bd..6e8ce19 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,9 @@ import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepOneValidationReque import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepOneValidationResult; import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepTwoValidationRequest; import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepTwoValidationResult; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepThreeValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepThreeValidationResult; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorWordPreviewResult; import com.sztzjy.linkCommerce.service.NewProductSurveyStepOneService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepOneService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepTwoService; @@ -46,6 +49,7 @@ import com.sztzjy.linkCommerce.service.ProductDevelopmentFactorsStepThreeService import com.sztzjy.linkCommerce.service.ProductDevelopmentFactorsStepFourService; import com.sztzjy.linkCommerce.service.ConsumerBehaviorStepOneService; import com.sztzjy.linkCommerce.service.ConsumerBehaviorStepTwoService; +import com.sztzjy.linkCommerce.service.ConsumerBehaviorStepThreeService; import com.sztzjy.linkCommerce.service.StudentTrainingAnswerService; import com.sztzjy.linkCommerce.util.ResultEntity; import io.swagger.annotations.Api; @@ -53,6 +57,7 @@ import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; @@ -103,6 +108,9 @@ public class StudentTrainingAnswerController { @Autowired private ConsumerBehaviorStepTwoService consumerBehaviorStepTwoService; + @Autowired + private ConsumerBehaviorStepThreeService consumerBehaviorStepThreeService; + @GetMapping("/{taskKey}") @ApiOperation("查询当前学生指定实训任务答案") public ResultEntity detail(@PathVariable String taskKey, @@ -285,6 +293,28 @@ public class StudentTrainingAnswerController { } } + @PostMapping("/consumer-behavior/step-3/validate") + @ApiOperation("消费者需求与行为第三步校验") + public ResultEntity validateConsumerBehaviorStepThree( + @RequestBody ConsumerBehaviorStepThreeValidationRequest answer, HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "校验通过", consumerBehaviorStepThreeService.validate(answer, TokenProvider.getJWTUser(request))); + } catch (ServiceException e) { + return new ResultEntity<>(e.getCode(), e.getMessage()); + } + } + + @PostMapping(value = "/consumer-behavior/step-3/word-preview", consumes = "multipart/form-data") + @ApiOperation("消费者需求与行为第三步Word预览") + public ResultEntity previewConsumerBehaviorStepThreeWord( + @RequestPart("file") MultipartFile file, HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "预览成功", consumerBehaviorStepThreeService.previewWord(file, 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/ConsumerBehaviorStepThreeValidationRequest.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorStepThreeValidationRequest.java new file mode 100644 index 0000000..fc76b61 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorStepThreeValidationRequest.java @@ -0,0 +1,29 @@ +package com.sztzjy.linkCommerce.entity.dto; + +import java.util.List; + +public class ConsumerBehaviorStepThreeValidationRequest { + private List questions; + private String importedDocumentUrl; + + public List getQuestions() { return questions; } + public void setQuestions(List questions) { this.questions = questions; } + public String getImportedDocumentUrl() { return importedDocumentUrl; } + public void setImportedDocumentUrl(String importedDocumentUrl) { this.importedDocumentUrl = importedDocumentUrl; } + + public static class Question { + private String section; + private String type; + private String title; + private String options; + + public String getSection() { return section; } + public void setSection(String section) { this.section = section; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getOptions() { return options; } + public void setOptions(String options) { this.options = options; } + } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorStepThreeValidationResult.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorStepThreeValidationResult.java new file mode 100644 index 0000000..b59b7c6 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorStepThreeValidationResult.java @@ -0,0 +1,11 @@ +package com.sztzjy.linkCommerce.entity.dto; + +public class ConsumerBehaviorStepThreeValidationResult { + 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/entity/dto/ConsumerBehaviorWordPreviewResult.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorWordPreviewResult.java new file mode 100644 index 0000000..d203399 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/ConsumerBehaviorWordPreviewResult.java @@ -0,0 +1,8 @@ +package com.sztzjy.linkCommerce.entity.dto; + +public class ConsumerBehaviorWordPreviewResult { + private String content; + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/ConsumerBehaviorStepThreeService.java b/src/main/java/com/sztzjy/linkCommerce/service/ConsumerBehaviorStepThreeService.java new file mode 100644 index 0000000..c5430fc --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/ConsumerBehaviorStepThreeService.java @@ -0,0 +1,12 @@ +package com.sztzjy.linkCommerce.service; + +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepThreeValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepThreeValidationResult; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorWordPreviewResult; +import org.springframework.web.multipart.MultipartFile; + +public interface ConsumerBehaviorStepThreeService { + ConsumerBehaviorStepThreeValidationResult validate(ConsumerBehaviorStepThreeValidationRequest request, JwtUser user); + ConsumerBehaviorWordPreviewResult previewWord(MultipartFile file, JwtUser user); +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/ConsumerBehaviorStepThreeServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/ConsumerBehaviorStepThreeServiceImpl.java new file mode 100644 index 0000000..486b255 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/ConsumerBehaviorStepThreeServiceImpl.java @@ -0,0 +1,120 @@ +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.dto.ConsumerBehaviorStepThreeValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepThreeValidationResult; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorWordPreviewResult; +import com.sztzjy.linkCommerce.service.ConsumerBehaviorStepThreeService; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFTable; +import org.apache.poi.xwpf.usermodel.XWPFTableCell; +import org.apache.poi.xwpf.usermodel.XWPFTableRow; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Service +public class ConsumerBehaviorStepThreeServiceImpl implements ConsumerBehaviorStepThreeService { + private static final Set REQUIRED_SECTIONS = new LinkedHashSet<>(Arrays.asList("基本信息", "购买行为", "需求偏好", "开放建议")); + private static final Set QUESTION_TYPES = new LinkedHashSet<>(Arrays.asList("SINGLE", "MULTIPLE", "LIKERT", "TEXT")); + private static final long MAX_WORD_FILE_SIZE = 10L * 1024 * 1024; + private static final int MAX_PREVIEW_LENGTH = 30000; + + @Override + public ConsumerBehaviorStepThreeValidationResult validate(ConsumerBehaviorStepThreeValidationRequest request, JwtUser user) { + requireStudent(user); + if (request != null && StringUtils.isNotBlank(request.getImportedDocumentUrl())) { + return validResult(); + } + List questions = request == null ? null : request.getQuestions(); + if (questions == null || questions.isEmpty()) { + throw badRequest("请完整设计问卷,或导入 Word 问卷"); + } + Set completedSections = new LinkedHashSet<>(); + boolean hasLikert = false; + for (ConsumerBehaviorStepThreeValidationRequest.Question question : questions) { + if (question == null || StringUtils.isBlank(question.getTitle()) + || !REQUIRED_SECTIONS.contains(StringUtils.trimToEmpty(question.getSection())) + || !QUESTION_TYPES.contains(StringUtils.trimToEmpty(question.getType()))) { + throw badRequest("请完整填写每道问卷题目的分区、题型和内容"); + } + if (("SINGLE".equals(question.getType()) || "MULTIPLE".equals(question.getType())) && StringUtils.isBlank(question.getOptions())) { + throw badRequest("单选题和多选题请填写选项"); + } + completedSections.add(question.getSection()); + hasLikert = hasLikert || ("需求偏好".equals(question.getSection()) && "LIKERT".equals(question.getType())); + } + if (!completedSections.containsAll(REQUIRED_SECTIONS) || !hasLikert) { + throw badRequest("问卷需包含基本信息、购买行为、需求偏好、开放建议四部分,并在需求偏好中包含 Likert 五级量表"); + } + return validResult(); + } + + @Override + public ConsumerBehaviorWordPreviewResult previewWord(MultipartFile file, JwtUser user) { + requireStudent(user); + validateWordFile(file); + try (XWPFDocument document = new XWPFDocument(file.getInputStream())) { + StringBuilder content = new StringBuilder(); + document.getParagraphs().forEach(paragraph -> appendLine(content, paragraph.getText())); + for (XWPFTable table : document.getTables()) { + for (XWPFTableRow row : table.getRows()) { + StringBuilder rowContent = new StringBuilder(); + for (XWPFTableCell cell : row.getTableCells()) { + if (rowContent.length() > 0) rowContent.append(" | "); + rowContent.append(StringUtils.trimToEmpty(cell.getText()).replaceAll("\\s+", " ")); + } + appendLine(content, rowContent.toString()); + } + } + if (content.length() > MAX_PREVIEW_LENGTH) content.setLength(MAX_PREVIEW_LENGTH); + ConsumerBehaviorWordPreviewResult result = new ConsumerBehaviorWordPreviewResult(); + result.setContent(content.toString().trim()); + return result; + } catch (IOException e) { + throw badRequest("Word 文档解析失败,请确认文件未损坏"); + } + } + + private ConsumerBehaviorStepThreeValidationResult validResult() { + ConsumerBehaviorStepThreeValidationResult result = new ConsumerBehaviorStepThreeValidationResult(); + result.setValid(true); + result.setMessage("校验通过"); + return result; + } + + private void validateWordFile(MultipartFile file) { + String fileName = file == null ? "" : StringUtils.defaultString(file.getOriginalFilename()); + if (file == null || file.isEmpty()) throw badRequest("请上传 Word 问卷"); + if (!StringUtils.endsWithIgnoreCase(fileName, ".docx")) throw badRequest("仅支持 .docx 格式的 Word 问卷预览"); + if (file.getSize() > MAX_WORD_FILE_SIZE) throw badRequest("Word 问卷大小不能超过 10MB"); + } + + private void appendLine(StringBuilder content, String value) { + String line = StringUtils.trimToEmpty(value); + if (line.isEmpty() || content.length() >= MAX_PREVIEW_LENGTH) return; + if (content.length() > 0) content.append('\n'); + content.append(line); + } + + private void requireStudent(JwtUser user) { + if (user == null || StringUtils.isBlank(user.getUserId())) { + throw new ServiceException(HttpStatus.UNAUTHORIZED, "请先登录后再提交"); + } + if (user.getRoleId() != 4) { + throw new ServiceException(HttpStatus.FORBIDDEN, "仅学生可提交实训答案"); + } + } + + private ServiceException badRequest(String message) { + return new ServiceException(HttpStatus.BAD_REQUEST, message); + } +} diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/ConsumerBehaviorStepThreeServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/ConsumerBehaviorStepThreeServiceImplTest.java new file mode 100644 index 0000000..ae933b0 --- /dev/null +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/ConsumerBehaviorStepThreeServiceImplTest.java @@ -0,0 +1,76 @@ +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.dto.ConsumerBehaviorStepThreeValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.ConsumerBehaviorStepThreeValidationResult; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConsumerBehaviorStepThreeServiceImplTest { + + @Test + void acceptsCompleteQuestionnaireForStudent() { + ConsumerBehaviorStepThreeServiceImpl service = new ConsumerBehaviorStepThreeServiceImpl(); + + ConsumerBehaviorStepThreeValidationResult result = service.validate(validRequest(), student()); + + assertTrue(result.isValid()); + } + + @Test + void rejectsQuestionnaireWithoutLikertOrOptions() { + ConsumerBehaviorStepThreeServiceImpl service = new ConsumerBehaviorStepThreeServiceImpl(); + ConsumerBehaviorStepThreeValidationRequest noLikert = validRequest(); + noLikert.getQuestions().get(2).setType("TEXT"); + assertThrows(ServiceException.class, () -> service.validate(noLikert, student())); + + ConsumerBehaviorStepThreeValidationRequest noOptions = validRequest(); + noOptions.getQuestions().get(0).setOptions(" "); + assertThrows(ServiceException.class, () -> service.validate(noOptions, student())); + } + + @Test + void rejectsNonStudent() { + ConsumerBehaviorStepThreeServiceImpl service = new ConsumerBehaviorStepThreeServiceImpl(); + + assertThrows(ServiceException.class, () -> service.validate(validRequest(), teacher())); + } + + private ConsumerBehaviorStepThreeValidationRequest validRequest() { + ConsumerBehaviorStepThreeValidationRequest request = new ConsumerBehaviorStepThreeValidationRequest(); + request.setQuestions(Arrays.asList( + question("基本信息", "SINGLE", "您的年龄处于以下哪个区间?", "18岁以下,18-25岁,26-35岁"), + question("购买行为", "MULTIPLE", "您通常通过什么渠道购买智能产品?", "电商平台,线下门店,社交平台"), + question("需求偏好", "LIKERT", "白噪声可定时关闭对您有多重要?", ""), + question("开放建议", "TEXT", "您希望智能助眠产品增加哪些功能?", ""))); + return request; + } + + private ConsumerBehaviorStepThreeValidationRequest.Question question(String section, String type, String title, String options) { + ConsumerBehaviorStepThreeValidationRequest.Question question = new ConsumerBehaviorStepThreeValidationRequest.Question(); + question.setSection(section); + question.setType(type); + question.setTitle(title); + question.setOptions(options); + return question; + } + + private JwtUser student() { + JwtUser user = new JwtUser(); + user.setUserId("stu-1"); + user.setRoleId(4); + return user; + } + + private JwtUser teacher() { + JwtUser user = new JwtUser(); + user.setUserId("teacher-1"); + user.setRoleId(2); + return user; + } +}