From 4424196fc957a9943123dc51aeb1c39f99b7fb30 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Mon, 24 Aug 2026 23:27:38 +0800 Subject: [PATCH] feat: complete market opportunity selection training workflow --- .../stu/StudentTrainingAnswerController.java | 34 +++++++++ ...itySelectionStepFiveValidationRequest.java | 27 +++++++ ...nitySelectionStepFiveValidationResult.java | 11 +++ ...nitySelectionStepSixValidationRequest.java | 8 ++ ...unitySelectionStepSixValidationResult.java | 11 +++ ...nitySelectionStepTwoValidationRequest.java | 17 ++++- ...etOpportunitySelectionStepFiveService.java | 9 +++ ...ketOpportunitySelectionStepSixService.java | 9 +++ ...portunitySelectionStepFiveServiceImpl.java | 69 +++++++++++++++++ ...portunitySelectionStepFourServiceImpl.java | 27 +++++++ ...pportunitySelectionStepOneServiceImpl.java | 4 +- ...pportunitySelectionStepSixServiceImpl.java | 74 +++++++++++++++++++ ...ortunitySelectionStepThreeServiceImpl.java | 15 +++- ...pportunitySelectionStepTwoServiceImpl.java | 69 ++++++++++++++--- .../StudentTrainingAnswerServiceImpl.java | 2 +- 15 files changed, 370 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepFiveValidationRequest.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepFiveValidationResult.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepSixValidationRequest.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepSixValidationResult.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/MarketOpportunitySelectionStepFiveService.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/MarketOpportunitySelectionStepSixService.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepFiveServiceImpl.java create mode 100644 src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepSixServiceImpl.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 e78b7c9..8022995 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java @@ -22,6 +22,10 @@ import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepThreeVal import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepThreeValidationResult; import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepFourValidationRequest; import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepFourValidationResult; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepFiveValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepFiveValidationResult; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepSixValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepSixValidationResult; import com.sztzjy.linkCommerce.entity.dto.NewProductSurveyStepTwoValidationRequest; import com.sztzjy.linkCommerce.entity.dto.NewProductSurveyStepTwoValidationResult; import com.sztzjy.linkCommerce.entity.dto.NewProductSurveyStepThreeValidationRequest; @@ -132,6 +136,8 @@ import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepOneService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepTwoService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepThreeService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepFourService; +import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepFiveService; +import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepSixService; import com.sztzjy.linkCommerce.service.NewProductSurveyStepTwoService; import com.sztzjy.linkCommerce.service.NewProductSurveyStepThreeService; import com.sztzjy.linkCommerce.service.NewProductSurveyStepFourService; @@ -233,6 +239,12 @@ public class StudentTrainingAnswerController { @Autowired private MarketOpportunitySelectionStepFourService marketOpportunitySelectionStepFourService; + @Autowired + private MarketOpportunitySelectionStepFiveService marketOpportunitySelectionStepFiveService; + + @Autowired + private MarketOpportunitySelectionStepSixService marketOpportunitySelectionStepSixService; + @Autowired private NewProductSurveyStepTwoService newProductSurveyStepTwoService; @@ -545,6 +557,28 @@ public class StudentTrainingAnswerController { } } + @PostMapping("/market-opportunity-selection/step-5/validate") + @ApiOperation("市场机会识别与选品第五步校验") + public ResultEntity validateMarketOpportunitySelectionStepFive( + @RequestBody MarketOpportunitySelectionStepFiveValidationRequest answer, HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "校验通过", marketOpportunitySelectionStepFiveService.validate(answer, TokenProvider.getJWTUser(request))); + } catch (ServiceException e) { + return new ResultEntity<>(e.getCode(), e.getMessage()); + } + } + + @PostMapping("/market-opportunity-selection/step-6/validate") + @ApiOperation("市场机会识别与选品第六步校验") + public ResultEntity validateMarketOpportunitySelectionStepSix( + @RequestBody MarketOpportunitySelectionStepSixValidationRequest answer, HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "校验通过", marketOpportunitySelectionStepSixService.validate(answer, TokenProvider.getJWTUser(request))); + } catch (ServiceException e) { + return new ResultEntity<>(e.getCode(), e.getMessage()); + } + } + @PostMapping("/new-product-survey/step-2/validate") @ApiOperation("新产品调查与分析第二步校验") public ResultEntity validateNewProductSurveyStepTwo( diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepFiveValidationRequest.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepFiveValidationRequest.java new file mode 100644 index 0000000..9019ccf --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepFiveValidationRequest.java @@ -0,0 +1,27 @@ +package com.sztzjy.linkCommerce.entity.dto; + +import java.math.BigDecimal; +import java.util.List; + +public class MarketOpportunitySelectionStepFiveValidationRequest { + private List insights; + + public List getInsights() { return insights; } + public void setInsights(List insights) { this.insights = insights; } + + public static class Insight { + private String productId; + private BigDecimal favorableRate; + private String painPoint; + private String unmetNeed; + + public String getProductId() { return productId; } + public void setProductId(String productId) { this.productId = productId; } + public BigDecimal getFavorableRate() { return favorableRate; } + public void setFavorableRate(BigDecimal favorableRate) { this.favorableRate = favorableRate; } + public String getPainPoint() { return painPoint; } + public void setPainPoint(String painPoint) { this.painPoint = painPoint; } + public String getUnmetNeed() { return unmetNeed; } + public void setUnmetNeed(String unmetNeed) { this.unmetNeed = unmetNeed; } + } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepFiveValidationResult.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepFiveValidationResult.java new file mode 100644 index 0000000..47c14fd --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepFiveValidationResult.java @@ -0,0 +1,11 @@ +package com.sztzjy.linkCommerce.entity.dto; + +public class MarketOpportunitySelectionStepFiveValidationResult { + 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/MarketOpportunitySelectionStepSixValidationRequest.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepSixValidationRequest.java new file mode 100644 index 0000000..b6d2dbf --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepSixValidationRequest.java @@ -0,0 +1,8 @@ +package com.sztzjy.linkCommerce.entity.dto; + +public class MarketOpportunitySelectionStepSixValidationRequest { + private String developmentConclusion; + + public String getDevelopmentConclusion() { return developmentConclusion; } + public void setDevelopmentConclusion(String developmentConclusion) { this.developmentConclusion = developmentConclusion; } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepSixValidationResult.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepSixValidationResult.java new file mode 100644 index 0000000..f3e1dff --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepSixValidationResult.java @@ -0,0 +1,11 @@ +package com.sztzjy.linkCommerce.entity.dto; + +public class MarketOpportunitySelectionStepSixValidationResult { + 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/MarketOpportunitySelectionStepTwoValidationRequest.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepTwoValidationRequest.java index 8d4c8aa..9baa49b 100644 --- a/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepTwoValidationRequest.java +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/MarketOpportunitySelectionStepTwoValidationRequest.java @@ -3,9 +3,20 @@ package com.sztzjy.linkCommerce.entity.dto; import java.util.List; public class MarketOpportunitySelectionStepTwoValidationRequest { - private List weights; - public List getWeights() { return weights; } - public void setWeights(List weights) { this.weights = weights; } + private List primaryWeights; + private List dimensionWeights; + public List getPrimaryWeights() { return primaryWeights; } + public void setPrimaryWeights(List primaryWeights) { this.primaryWeights = primaryWeights; } + public List getDimensionWeights() { return dimensionWeights; } + public void setDimensionWeights(List dimensionWeights) { this.dimensionWeights = dimensionWeights; } + public static class PrimaryWeight { + private String group; + private Integer weight; + public String getGroup() { return group; } + public void setGroup(String group) { this.group = group; } + public Integer getWeight() { return weight; } + public void setWeight(Integer weight) { this.weight = weight; } + } public static class Weight { private String id; private Integer weight; diff --git a/src/main/java/com/sztzjy/linkCommerce/service/MarketOpportunitySelectionStepFiveService.java b/src/main/java/com/sztzjy/linkCommerce/service/MarketOpportunitySelectionStepFiveService.java new file mode 100644 index 0000000..56850ae --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/MarketOpportunitySelectionStepFiveService.java @@ -0,0 +1,9 @@ +package com.sztzjy.linkCommerce.service; + +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepFiveValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepFiveValidationResult; + +public interface MarketOpportunitySelectionStepFiveService { + MarketOpportunitySelectionStepFiveValidationResult validate(MarketOpportunitySelectionStepFiveValidationRequest request, JwtUser user); +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/MarketOpportunitySelectionStepSixService.java b/src/main/java/com/sztzjy/linkCommerce/service/MarketOpportunitySelectionStepSixService.java new file mode 100644 index 0000000..f086c2b --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/MarketOpportunitySelectionStepSixService.java @@ -0,0 +1,9 @@ +package com.sztzjy.linkCommerce.service; + +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepSixValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepSixValidationResult; + +public interface MarketOpportunitySelectionStepSixService { + MarketOpportunitySelectionStepSixValidationResult validate(MarketOpportunitySelectionStepSixValidationRequest request, JwtUser user); +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepFiveServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepFiveServiceImpl.java new file mode 100644 index 0000000..ca6ba10 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepFiveServiceImpl.java @@ -0,0 +1,69 @@ +package com.sztzjy.linkCommerce.service.impl; + +import com.alibaba.fastjson.JSON; +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.MarketOpportunitySelectionStepFiveValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepFiveValidationResult; +import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepFiveService; +import com.sztzjy.linkCommerce.service.StudentTrainingAnswerService; +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.math.BigDecimal; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +public class MarketOpportunitySelectionStepFiveServiceImpl implements MarketOpportunitySelectionStepFiveService { + private static final String TASK_KEY = "market-opportunity-selection"; + private static final Set PRODUCT_IDS = new LinkedHashSet<>(Arrays.asList("smart-fitness-mirror", "smart-bathroom-mirror", "smart-beauty-mirror")); + + @Autowired private StudentTrainingAnswerService studentTrainingAnswerService; + + @Override + public MarketOpportunitySelectionStepFiveValidationResult validate(MarketOpportunitySelectionStepFiveValidationRequest request, JwtUser user) { + requireStudent(user); + StudentTrainingAnswer savedAnswer = studentTrainingAnswerService.get(TASK_KEY, null, user); + if (savedAnswer == null || StringUtils.isBlank(savedAnswer.getStep4Answer())) throw badRequest("请先完成并保存第 4 步数据可视化"); + requireCompletedStepFour(savedAnswer.getStep4Answer()); + + List insights = request == null ? null : request.getInsights(); + if (insights == null || insights.size() != PRODUCT_IDS.size()) throw badRequest("请完整填写三类产品的用户需求洞察"); + Set filledProducts = new LinkedHashSet<>(); + for (MarketOpportunitySelectionStepFiveValidationRequest.Insight insight : insights) { + if (insight == null || !PRODUCT_IDS.contains(insight.getProductId()) || !filledProducts.add(insight.getProductId())) throw badRequest("用户需求洞察必须包含且仅包含三类产品"); + BigDecimal favorableRate = insight.getFavorableRate(); + if (favorableRate == null || favorableRate.compareTo(BigDecimal.ZERO) < 0 || favorableRate.compareTo(new BigDecimal("100")) > 0 || favorableRate.scale() > 2) throw badRequest("好评率须为 0 至 100,最多保留两位小数"); + if (StringUtils.isBlank(insight.getPainPoint()) || StringUtils.isBlank(insight.getUnmetNeed())) throw badRequest("请填写每类产品的用户痛点和未被满足的需求"); + } + if (!filledProducts.equals(PRODUCT_IDS)) throw badRequest("用户需求洞察必须包含且仅包含三类产品"); + + MarketOpportunitySelectionStepFiveValidationResult result = new MarketOpportunitySelectionStepFiveValidationResult(); + result.setValid(true); + result.setMessage("用户需求洞察校验通过"); + return result; + } + + @SuppressWarnings("unchecked") + private void requireCompletedStepFour(String answerJson) { + Map outcome = JSON.parseObject(answerJson, LinkedHashMap.class); + Object rawVisualization = outcome == null ? null : outcome.get("visualization"); + Object url = rawVisualization instanceof Map ? ((Map) rawVisualization).get("url") : null; + if (!(rawVisualization instanceof Map) || url == null || StringUtils.isBlank(String.valueOf(url))) throw badRequest("请先完成并保存第 4 步数据可视化"); + } + + 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/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepFourServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepFourServiceImpl.java index 140e62c..0af7792 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepFourServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepFourServiceImpl.java @@ -1,5 +1,6 @@ package com.sztzjy.linkCommerce.service.impl; +import com.alibaba.fastjson.JSON; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer; @@ -12,9 +13,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + @Service public class MarketOpportunitySelectionStepFourServiceImpl implements MarketOpportunitySelectionStepFourService { private static final String TASK_KEY = "market-opportunity-selection"; + private static final Set PRODUCT_IDS = new LinkedHashSet<>(Arrays.asList("smart-fitness-mirror", "smart-bathroom-mirror", "smart-beauty-mirror")); @Autowired private StudentTrainingAnswerService studentTrainingAnswerService; @@ -24,9 +33,27 @@ public class MarketOpportunitySelectionStepFourServiceImpl implements MarketOppo if (user.getRoleId() != 4) throw new ServiceException(HttpStatus.FORBIDDEN, "仅学生可进行实训校验"); StudentTrainingAnswer savedAnswer = studentTrainingAnswerService.get(TASK_KEY, null, user); if (savedAnswer == null || StringUtils.isBlank(savedAnswer.getStep3Answer())) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并保存第 3 步综合得分计算"); + requireCompletedStepThree(savedAnswer.getStep3Answer()); if (request == null || StringUtils.isBlank(request.getVisualizationUrl())) throw new ServiceException(HttpStatus.BAD_REQUEST, "请上传数据可视化结果图"); MarketOpportunitySelectionStepFourValidationResult result = new MarketOpportunitySelectionStepFourValidationResult(); result.setValid(true); result.setMessage("校验通过"); return result; } + + @SuppressWarnings("unchecked") + private void requireCompletedStepThree(String answerJson) { + Map outcome = JSON.parseObject(answerJson, LinkedHashMap.class); + Object rawScores = outcome == null ? null : outcome.get("scores"); + if (!(rawScores instanceof Map) || ((Map) rawScores).size() != PRODUCT_IDS.size()) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 3 步综合得分计算"); + Map scores = (Map) rawScores; + if (!scores.keySet().equals(PRODUCT_IDS)) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 3 步综合得分计算"); + for (Object value : scores.values()) { + try { + BigDecimal score = new BigDecimal(String.valueOf(value)); + if (score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("100")) > 0 || score.scale() > 2) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 3 步综合得分计算"); + } catch (NumberFormatException error) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 3 步综合得分计算"); + } + } + } } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepOneServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepOneServiceImpl.java index acb61d8..9152fda 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepOneServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepOneServiceImpl.java @@ -33,7 +33,7 @@ public class MarketOpportunitySelectionStepOneServiceImpl implements MarketOppor @PostConstruct public void initializeStandardAnswers() { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS training_step_standard_answer (id varchar(64) NOT NULL COMMENT 'primary id',task_key varchar(128) NOT NULL COMMENT 'training task key',step_no int NOT NULL COMMENT 'step number',answer_json longtext NOT NULL COMMENT 'standard answer json',create_time datetime NULL,update_time datetime NULL,PRIMARY KEY (id),UNIQUE KEY uk_training_step_standard_answer (task_key, step_no)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='training step standard answer'"); - jdbcTemplate.update("INSERT IGNORE INTO training_step_standard_answer (id, task_key, step_no, answer_json, create_time, update_time) VALUES (?, ?, ?, ?, NOW(), NOW())", "market-opportunity-selection-step-1", TASK_KEY, STEP_NO, JSON.toJSONString(DEFAULT_ANSWERS)); + jdbcTemplate.update("INSERT INTO training_step_standard_answer (id, task_key, step_no, answer_json, create_time, update_time) VALUES (?, ?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE answer_json = VALUES(answer_json), update_time = NOW()", "market-opportunity-selection-step-1", TASK_KEY, STEP_NO, JSON.toJSONString(DEFAULT_ANSWERS)); } @Override @@ -72,7 +72,7 @@ public class MarketOpportunitySelectionStepOneServiceImpl implements MarketOppor } private static Map createDefaultAnswers() { Map answers = new LinkedHashMap<>(); - answers.put("search-count", "市场规模"); answers.put("demand-supply-ratio", "竞争强度"); answers.put("transaction-amount", "市场规模"); answers.put("transaction-growth", "市场潜力"); answers.put("category-transaction-growth", "市场潜力"); answers.put("online-products", "竞争强度"); answers.put("online-merchants", "竞争强度"); answers.put("organic-traffic-ratio", "运营难度"); + answers.put("search-count", "市场规模"); answers.put("demand-supply-ratio", "竞争强度"); answers.put("transaction-amount", "市场规模"); answers.put("transaction-growth", "市场潜力"); answers.put("category-transaction-growth", "市场潜力"); answers.put("online-products", "竞争强度"); answers.put("online-merchants", "竞争强度"); answers.put("organic-traffic-ratio", "运营难度"); answers.put("return-rate", "运营难度"); return answers; } } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepSixServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepSixServiceImpl.java new file mode 100644 index 0000000..d430971 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepSixServiceImpl.java @@ -0,0 +1,74 @@ +package com.sztzjy.linkCommerce.service.impl; + +import com.alibaba.fastjson.JSON; +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.MarketOpportunitySelectionStepSixValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.MarketOpportunitySelectionStepSixValidationResult; +import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepSixService; +import com.sztzjy.linkCommerce.service.StudentTrainingAnswerService; +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.math.BigDecimal; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +@Service +public class MarketOpportunitySelectionStepSixServiceImpl implements MarketOpportunitySelectionStepSixService { + private static final String TASK_KEY = "market-opportunity-selection"; + private static final Set PRODUCT_IDS = new LinkedHashSet<>(Arrays.asList("smart-fitness-mirror", "smart-bathroom-mirror", "smart-beauty-mirror")); + + @Autowired private StudentTrainingAnswerService studentTrainingAnswerService; + + @Override + public MarketOpportunitySelectionStepSixValidationResult validate(MarketOpportunitySelectionStepSixValidationRequest request, JwtUser user) { + requireStudent(user); + StudentTrainingAnswer savedAnswer = studentTrainingAnswerService.get(TASK_KEY, null, user); + if (savedAnswer == null || StringUtils.isBlank(savedAnswer.getDynamicStepAnswers())) throw badRequest("请先完成并保存第 5 步用户需求洞察"); + requireCompletedStepFive(savedAnswer.getDynamicStepAnswers()); + if (request == null || StringUtils.isBlank(request.getDevelopmentConclusion())) throw badRequest("请填写建议开发的产品及具体原因"); + + MarketOpportunitySelectionStepSixValidationResult result = new MarketOpportunitySelectionStepSixValidationResult(); + result.setValid(true); + result.setMessage("选品结论校验通过"); + return result; + } + + @SuppressWarnings("unchecked") + private void requireCompletedStepFive(String answerJson) { + Map dynamicAnswers = JSON.parseObject(answerJson, LinkedHashMap.class); + Object rawStepFive = dynamicAnswers == null ? null : dynamicAnswers.get("step5"); + Object rawInsights = rawStepFive instanceof Map ? ((Map) rawStepFive).get("reviewInsights") : null; + if (!(rawInsights instanceof Map)) throw badRequest("请先完成并保存第 5 步用户需求洞察"); + Map insights = (Map) rawInsights; + if (!insights.keySet().equals(PRODUCT_IDS)) throw badRequest("请先完成并保存第 5 步用户需求洞察"); + for (Object rawInsight : insights.values()) { + if (!(rawInsight instanceof Map)) throw badRequest("请先完成并保存第 5 步用户需求洞察"); + Map insight = (Map) rawInsight; + Object rawRate = insight.get("favorableRate"); + try { + BigDecimal rate = new BigDecimal(String.valueOf(rawRate)); + if (rawRate == null || rate.compareTo(BigDecimal.ZERO) < 0 || rate.compareTo(new BigDecimal("100")) > 0 || rate.scale() > 2) throw badRequest("请先完成并保存第 5 步用户需求洞察"); + } catch (NumberFormatException error) { + throw badRequest("请先完成并保存第 5 步用户需求洞察"); + } + Object painPoint = insight.get("painPoint"); + Object unmetNeed = insight.get("unmetNeed"); + if (painPoint == null || unmetNeed == null || StringUtils.isBlank(String.valueOf(painPoint)) || StringUtils.isBlank(String.valueOf(unmetNeed))) throw badRequest("请先完成并保存第 5 步用户需求洞察"); + } + } + + 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/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepThreeServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepThreeServiceImpl.java index 0e83dd9..63c7f7b 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepThreeServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepThreeServiceImpl.java @@ -1,5 +1,6 @@ package com.sztzjy.linkCommerce.service.impl; +import com.alibaba.fastjson.JSON; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer; @@ -14,7 +15,9 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; @Service @@ -29,13 +32,23 @@ public class MarketOpportunitySelectionStepThreeServiceImpl implements MarketOpp if (user.getRoleId() != 4) throw new ServiceException(HttpStatus.FORBIDDEN, "仅学生可进行实训校验"); StudentTrainingAnswer savedAnswer = studentTrainingAnswerService.get(TASK_KEY, null, user); if (savedAnswer == null || StringUtils.isBlank(savedAnswer.getStep2Answer())) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并保存第 2 步指标权重设定"); + requireCompletedStepTwo(savedAnswer.getStep2Answer()); if (request == null || request.getScores() == null || request.getScores().size() != PRODUCT_IDS.size()) throw new ServiceException(HttpStatus.BAD_REQUEST, "请为三类产品填写综合得分"); Set submittedIds = new LinkedHashSet<>(); for (MarketOpportunitySelectionStepThreeValidationRequest.Score item : request.getScores()) { String id = item == null ? "" : StringUtils.trimToEmpty(item.getId()); BigDecimal score = item == null ? null : item.getScore(); - if (!PRODUCT_IDS.contains(id) || !submittedIds.add(id) || score == null || score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("100")) > 0) throw new ServiceException(HttpStatus.BAD_REQUEST, "综合得分需为 0 至 100 的数值"); + if (!PRODUCT_IDS.contains(id) || !submittedIds.add(id) || score == null || score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("100")) > 0 || score.scale() > 2) throw new ServiceException(HttpStatus.BAD_REQUEST, "综合得分需为 0 至 100 的数值,且最多保留两位小数"); } if (!submittedIds.equals(PRODUCT_IDS)) throw new ServiceException(HttpStatus.BAD_REQUEST, "提交的产品品类不完整"); MarketOpportunitySelectionStepThreeValidationResult result = new MarketOpportunitySelectionStepThreeValidationResult(); result.setValid(true); result.setMessage("校验通过"); return result; } + + @SuppressWarnings("unchecked") + private void requireCompletedStepTwo(String answerJson) { + Map outcome = JSON.parseObject(answerJson, LinkedHashMap.class); + if (outcome == null || !(outcome.get("primaryWeights") instanceof Map) || !(outcome.get("dimensionWeights") instanceof Map)) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 2 步指标权重设定"); + Map primaryWeights = (Map) outcome.get("primaryWeights"); + Map dimensionWeights = (Map) outcome.get("dimensionWeights"); + if (primaryWeights.size() != 4 || dimensionWeights.size() != 9) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 2 步指标权重设定"); + } } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepTwoServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepTwoServiceImpl.java index f4ad38e..498e10f 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepTwoServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/MarketOpportunitySelectionStepTwoServiceImpl.java @@ -1,5 +1,6 @@ package com.sztzjy.linkCommerce.service.impl; +import com.alibaba.fastjson.JSON; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer; @@ -13,13 +14,18 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Set; @Service public class MarketOpportunitySelectionStepTwoServiceImpl implements MarketOpportunitySelectionStepTwoService { private static final String TASK_KEY = "market-opportunity-selection"; - private static final Set INDICATOR_IDS = new LinkedHashSet<>(Arrays.asList("search-count", "transaction-amount", "transaction-growth", "category-transaction-growth", "demand-supply-ratio", "online-products", "online-merchants", "organic-traffic-ratio")); + private static final Set INDICATOR_IDS = new LinkedHashSet<>(Arrays.asList("search-count", "demand-supply-ratio", "transaction-amount", "transaction-growth", "category-transaction-growth", "online-products", "online-merchants", "organic-traffic-ratio", "return-rate")); + private static final Set PRIMARY_GROUPS = new LinkedHashSet<>(Arrays.asList("市场规模", "市场潜力", "竞争强度", "运营难度")); + private static final Map INDICATOR_GROUPS = createIndicatorGroups(); @Autowired private StudentTrainingAnswerService studentTrainingAnswerService; @Override @@ -28,15 +34,60 @@ public class MarketOpportunitySelectionStepTwoServiceImpl implements MarketOppor 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, "请先完成并保存第 1 步指标分类"); - if (request == null || request.getWeights() == null || request.getWeights().size() != INDICATOR_IDS.size()) throw new ServiceException(HttpStatus.BAD_REQUEST, "请为全部二级指标填写权重"); - Set submittedIds = new LinkedHashSet<>(); int totalWeight = 0; - for (MarketOpportunitySelectionStepTwoValidationRequest.Weight item : request.getWeights()) { + requireCompletedStepOne(savedAnswer.getStep1Answer()); + if (request == null || request.getPrimaryWeights() == null || request.getDimensionWeights() == null) throw new ServiceException(HttpStatus.BAD_REQUEST, "请填写一级指标与维度内权重"); + Map primaryWeights = validatePrimaryWeights(request.getPrimaryWeights()); + Map dimensionWeights = validateDimensionWeights(request.getDimensionWeights()); + validateCorrespondingWeightTotal(primaryWeights, dimensionWeights); + MarketOpportunitySelectionStepTwoValidationResult result = new MarketOpportunitySelectionStepTwoValidationResult(); result.setValid(true); result.setMessage("校验通过"); return result; + } + + private Map validatePrimaryWeights(List items) { + if (items.size() != PRIMARY_GROUPS.size()) throw new ServiceException(HttpStatus.BAD_REQUEST, "请为四类一级指标填写权重"); + Map weights = new LinkedHashMap<>(); int total = 0; + for (MarketOpportunitySelectionStepTwoValidationRequest.PrimaryWeight item : items) { + String group = item == null ? "" : StringUtils.trimToEmpty(item.getGroup()); Integer weight = item == null ? null : item.getWeight(); + if (!PRIMARY_GROUPS.contains(group) || weights.put(group, weight) != null || !isValidWeight(weight)) throw new ServiceException(HttpStatus.BAD_REQUEST, "一级指标权重需为 1 至 100 的整数"); + total += weight; + } + if (!weights.keySet().equals(PRIMARY_GROUPS) || total != 100) throw new ServiceException(HttpStatus.BAD_REQUEST, "一级指标权重合计必须为 100%"); + return weights; + } + + private Map validateDimensionWeights(List items) { + if (items.size() != INDICATOR_IDS.size()) throw new ServiceException(HttpStatus.BAD_REQUEST, "请为全部二级指标填写维度内权重"); + Map weights = new LinkedHashMap<>(); + for (MarketOpportunitySelectionStepTwoValidationRequest.Weight item : items) { String id = item == null ? "" : StringUtils.trimToEmpty(item.getId()); Integer weight = item == null ? null : item.getWeight(); - if (!INDICATOR_IDS.contains(id) || !submittedIds.add(id) || weight == null || weight < 1 || weight > 100) throw new ServiceException(HttpStatus.BAD_REQUEST, "二级指标权重需为 1 至 100 的整数"); - totalWeight += weight; + if (!INDICATOR_IDS.contains(id) || weights.put(id, weight) != null || !isValidWeight(weight)) throw new ServiceException(HttpStatus.BAD_REQUEST, "维度内权重需为 1 至 100 的整数"); } - if (!submittedIds.equals(INDICATOR_IDS)) throw new ServiceException(HttpStatus.BAD_REQUEST, "提交的二级指标不完整"); - if (totalWeight != 100) throw new ServiceException(HttpStatus.BAD_REQUEST, "二级指标权重合计必须为 100%"); - MarketOpportunitySelectionStepTwoValidationResult result = new MarketOpportunitySelectionStepTwoValidationResult(); result.setValid(true); result.setMessage("校验通过"); return result; + if (!weights.keySet().equals(INDICATOR_IDS)) throw new ServiceException(HttpStatus.BAD_REQUEST, "提交的二级指标不完整"); + for (String group : PRIMARY_GROUPS) { + int total = INDICATOR_IDS.stream().filter((id) -> group.equals(INDICATOR_GROUPS.get(id))).mapToInt(weights::get).sum(); + if (total != 100) throw new ServiceException(HttpStatus.BAD_REQUEST, group + "的维度内权重合计必须为 100%"); + } + return weights; + } + + private void validateCorrespondingWeightTotal(Map primaryWeights, Map dimensionWeights) { + int totalHundredths = INDICATOR_IDS.stream().mapToInt((id) -> primaryWeights.get(INDICATOR_GROUPS.get(id)) * dimensionWeights.get(id)).sum(); + if (totalHundredths != 10000) throw new ServiceException(HttpStatus.BAD_REQUEST, "对应总权重合计必须为 100%"); + } + + @SuppressWarnings("unchecked") + private void requireCompletedStepOne(String answerJson) { + Map outcome = JSON.parseObject(answerJson, LinkedHashMap.class); + Object rawAnswers = outcome == null ? null : outcome.get("answers"); + if (!(rawAnswers instanceof Map)) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 1 步指标分类"); + Map answers = (Map) rawAnswers; + if (answers.size() != INDICATOR_IDS.size()) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 1 步指标分类"); + for (String id : INDICATOR_IDS) if (!INDICATOR_GROUPS.get(id).equals(String.valueOf(answers.get(id)))) throw new ServiceException(HttpStatus.BAD_REQUEST, "请先完成并通过第 1 步指标分类"); + } + + private boolean isValidWeight(Integer weight) { return weight != null && weight >= 1 && weight <= 100; } + private static Map createIndicatorGroups() { + Map groups = new LinkedHashMap<>(); + groups.put("search-count", "市场规模"); groups.put("demand-supply-ratio", "竞争强度"); groups.put("transaction-amount", "市场规模"); groups.put("transaction-growth", "市场潜力"); groups.put("category-transaction-growth", "市场潜力"); groups.put("online-products", "竞争强度"); groups.put("online-merchants", "竞争强度"); groups.put("organic-traffic-ratio", "运营难度"); groups.put("return-rate", "运营难度"); + return groups; } } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java index 2e7b85b..29fe211 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java @@ -206,7 +206,7 @@ public class StudentTrainingAnswerServiceImpl implements StudentTrainingAnswerSe boolean submitted = ACTION_SUBMIT.equals(StringUtils.upperCase(StringUtils.trimToEmpty(request.getSaveAction()))); record.setSubmitted(submitted); - record.setCurrentStep(submitted ? taskStepCount(task) : normalizeCurrentStepValue(request.getCurrentStep())); + record.setCurrentStep(submitted ? Math.max(taskStepCount(task), normalizeCurrentStepValue(request.getCurrentStep())) : normalizeCurrentStepValue(request.getCurrentStep())); record.setProgressStatus(submitted ? STATUS_COMPLETED : resolveProgressStatus(record)); if (submitted) { record.setSubmitTime(new Date());