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 5f892de..c1c3246 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java @@ -28,6 +28,8 @@ import com.sztzjy.linkCommerce.entity.dto.ProductDevelopmentFactorsStepThreeVali import com.sztzjy.linkCommerce.entity.dto.ProductDevelopmentFactorsStepThreeValidationResult; import com.sztzjy.linkCommerce.entity.dto.ProductDevelopmentFactorsStepFourValidationRequest; import com.sztzjy.linkCommerce.entity.dto.ProductDevelopmentFactorsStepFourValidationResult; +import com.sztzjy.linkCommerce.entity.dto.BusinessFeasibilityStepOneValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.BusinessFeasibilityStepOneValidationResult; import com.sztzjy.linkCommerce.service.NewProductSurveyStepOneService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepOneService; import com.sztzjy.linkCommerce.service.MarketOpportunitySelectionStepTwoService; @@ -40,6 +42,7 @@ import com.sztzjy.linkCommerce.service.ProductDevelopmentFactorsStepOneService; import com.sztzjy.linkCommerce.service.ProductDevelopmentFactorsStepTwoService; import com.sztzjy.linkCommerce.service.ProductDevelopmentFactorsStepThreeService; import com.sztzjy.linkCommerce.service.ProductDevelopmentFactorsStepFourService; +import com.sztzjy.linkCommerce.service.BusinessFeasibilityStepOneService; import com.sztzjy.linkCommerce.service.StudentTrainingAnswerService; import com.sztzjy.linkCommerce.util.ResultEntity; import io.swagger.annotations.Api; @@ -91,6 +94,9 @@ public class StudentTrainingAnswerController { @Autowired private ProductDevelopmentFactorsStepFourService productDevelopmentFactorsStepFourService; + @Autowired + private BusinessFeasibilityStepOneService businessFeasibilityStepOneService; + @GetMapping("/{taskKey}") @ApiOperation("查询当前学生指定实训任务答案") public ResultEntity detail(@PathVariable String taskKey, @@ -251,6 +257,17 @@ public class StudentTrainingAnswerController { catch (ServiceException e) { return new ResultEntity<>(e.getCode(), e.getMessage()); } } + @PostMapping("/business-feasibility/step-1/validate") + @ApiOperation("商业可行性论证第一步校验") + public ResultEntity validateBusinessFeasibilityStepOne( + @RequestBody BusinessFeasibilityStepOneValidationRequest answer, HttpServletRequest request) { + try { + return new ResultEntity<>(HttpStatus.OK, "校验通过", businessFeasibilityStepOneService.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/BusinessFeasibilityStepOneValidationRequest.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/BusinessFeasibilityStepOneValidationRequest.java new file mode 100644 index 0000000..8ca37b9 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/BusinessFeasibilityStepOneValidationRequest.java @@ -0,0 +1,116 @@ +package com.sztzjy.linkCommerce.entity.dto; + +import java.math.BigDecimal; +import java.util.List; + +public class BusinessFeasibilityStepOneValidationRequest { + private ProjectOverview projectOverview; + private MarketAnalysis marketAnalysis; + private List targetUsers; + private List businessModels; + private List costs; + private BigDecimal targetPrice; + private List risks; + + public ProjectOverview getProjectOverview() { return projectOverview; } + public void setProjectOverview(ProjectOverview projectOverview) { this.projectOverview = projectOverview; } + public MarketAnalysis getMarketAnalysis() { return marketAnalysis; } + public void setMarketAnalysis(MarketAnalysis marketAnalysis) { this.marketAnalysis = marketAnalysis; } + public List getTargetUsers() { return targetUsers; } + public void setTargetUsers(List targetUsers) { this.targetUsers = targetUsers; } + public List getBusinessModels() { return businessModels; } + public void setBusinessModels(List businessModels) { this.businessModels = businessModels; } + public List getCosts() { return costs; } + public void setCosts(List costs) { this.costs = costs; } + public BigDecimal getTargetPrice() { return targetPrice; } + public void setTargetPrice(BigDecimal targetPrice) { this.targetPrice = targetPrice; } + public List getRisks() { return risks; } + public void setRisks(List risks) { this.risks = risks; } + + public static class ProjectOverview { + private String projectBackground; + private String productPositioning; + private String projectGoals; + private String macroEnvironmentConclusion; + private String competitiveLandscapeConclusion; + public String getProjectBackground() { return projectBackground; } + public void setProjectBackground(String projectBackground) { this.projectBackground = projectBackground; } + public String getProductPositioning() { return productPositioning; } + public void setProductPositioning(String productPositioning) { this.productPositioning = productPositioning; } + public String getProjectGoals() { return projectGoals; } + public void setProjectGoals(String projectGoals) { this.projectGoals = projectGoals; } + public String getMacroEnvironmentConclusion() { return macroEnvironmentConclusion; } + public void setMacroEnvironmentConclusion(String macroEnvironmentConclusion) { this.macroEnvironmentConclusion = macroEnvironmentConclusion; } + public String getCompetitiveLandscapeConclusion() { return competitiveLandscapeConclusion; } + public void setCompetitiveLandscapeConclusion(String competitiveLandscapeConclusion) { this.competitiveLandscapeConclusion = competitiveLandscapeConclusion; } + } + + public static class MarketAnalysis { + private BigDecimal annualMarketSize; + private BigDecimal monthlyGrowthRate; + private BigDecimal topBrandMarketShare; + private String competitorsPriceRange; + private BigDecimal marketConcentrationCr3; + public BigDecimal getAnnualMarketSize() { return annualMarketSize; } + public void setAnnualMarketSize(BigDecimal annualMarketSize) { this.annualMarketSize = annualMarketSize; } + public BigDecimal getMonthlyGrowthRate() { return monthlyGrowthRate; } + public void setMonthlyGrowthRate(BigDecimal monthlyGrowthRate) { this.monthlyGrowthRate = monthlyGrowthRate; } + public BigDecimal getTopBrandMarketShare() { return topBrandMarketShare; } + public void setTopBrandMarketShare(BigDecimal topBrandMarketShare) { this.topBrandMarketShare = topBrandMarketShare; } + public String getCompetitorsPriceRange() { return competitorsPriceRange; } + public void setCompetitorsPriceRange(String competitorsPriceRange) { this.competitorsPriceRange = competitorsPriceRange; } + public BigDecimal getMarketConcentrationCr3() { return marketConcentrationCr3; } + public void setMarketConcentrationCr3(BigDecimal marketConcentrationCr3) { this.marketConcentrationCr3 = marketConcentrationCr3; } + } + + public static class TargetUser { + private String userGroup; + private String scenario; + private String coreNeed; + private String spendingPower; + public String getUserGroup() { return userGroup; } + public void setUserGroup(String userGroup) { this.userGroup = userGroup; } + public String getScenario() { return scenario; } + public void setScenario(String scenario) { this.scenario = scenario; } + public String getCoreNeed() { return coreNeed; } + public void setCoreNeed(String coreNeed) { this.coreNeed = coreNeed; } + public String getSpendingPower() { return spendingPower; } + public void setSpendingPower(String spendingPower) { this.spendingPower = spendingPower; } + } + + public static class BusinessModel { + private String module; + private String strategy; + private String explanation; + public String getModule() { return module; } + public void setModule(String module) { this.module = module; } + public String getStrategy() { return strategy; } + public void setStrategy(String strategy) { this.strategy = strategy; } + public String getExplanation() { return explanation; } + public void setExplanation(String explanation) { this.explanation = explanation; } + } + + public static class CostItem { + private String id; + private BigDecimal amount; + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + } + + public static class Risk { + private String type; + private String description; + private String impact; + private String response; + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getImpact() { return impact; } + public void setImpact(String impact) { this.impact = impact; } + public String getResponse() { return response; } + public void setResponse(String response) { this.response = response; } + } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/BusinessFeasibilityStepOneValidationResult.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/BusinessFeasibilityStepOneValidationResult.java new file mode 100644 index 0000000..0e92dfb --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/BusinessFeasibilityStepOneValidationResult.java @@ -0,0 +1,21 @@ +package com.sztzjy.linkCommerce.entity.dto; + +import java.math.BigDecimal; + +public class BusinessFeasibilityStepOneValidationResult { + private boolean valid; + private String message; + private BigDecimal unitTotalCost; + private BigDecimal unitGrossProfit; + private BigDecimal grossMargin; + 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; } + public BigDecimal getUnitTotalCost() { return unitTotalCost; } + public void setUnitTotalCost(BigDecimal unitTotalCost) { this.unitTotalCost = unitTotalCost; } + public BigDecimal getUnitGrossProfit() { return unitGrossProfit; } + public void setUnitGrossProfit(BigDecimal unitGrossProfit) { this.unitGrossProfit = unitGrossProfit; } + public BigDecimal getGrossMargin() { return grossMargin; } + public void setGrossMargin(BigDecimal grossMargin) { this.grossMargin = grossMargin; } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/BusinessFeasibilityStepOneService.java b/src/main/java/com/sztzjy/linkCommerce/service/BusinessFeasibilityStepOneService.java new file mode 100644 index 0000000..fb5128b --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/BusinessFeasibilityStepOneService.java @@ -0,0 +1,9 @@ +package com.sztzjy.linkCommerce.service; + +import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.dto.BusinessFeasibilityStepOneValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.BusinessFeasibilityStepOneValidationResult; + +public interface BusinessFeasibilityStepOneService { + BusinessFeasibilityStepOneValidationResult validate(BusinessFeasibilityStepOneValidationRequest request, JwtUser user); +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/BusinessFeasibilityStepOneServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/BusinessFeasibilityStepOneServiceImpl.java new file mode 100644 index 0000000..4ec8e85 --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/BusinessFeasibilityStepOneServiceImpl.java @@ -0,0 +1,102 @@ +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.BusinessFeasibilityStepOneValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.BusinessFeasibilityStepOneValidationResult; +import com.sztzjy.linkCommerce.service.BusinessFeasibilityStepOneService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Service +public class BusinessFeasibilityStepOneServiceImpl implements BusinessFeasibilityStepOneService { + private static final List COST_IDS = Arrays.asList("hardware-bom", "manufacturing", "rd-amortization", "channel-marketing", "logistics-warehouse"); + private static final List RISK_TYPES = Arrays.asList("技术风险", "市场风险", "供应链风险", "资金风险"); + private static final Set IMPACTS = new LinkedHashSet<>(Arrays.asList("高", "中", "低")); + + @Override + public BusinessFeasibilityStepOneValidationResult validate(BusinessFeasibilityStepOneValidationRequest request, JwtUser user) { + assertStudent(user); + if (request == null) throw badRequest("请完成商业可行性论证第 1 步"); + validateOverview(request.getProjectOverview()); + validateMarket(request.getMarketAnalysis()); + validateTargetUsers(request.getTargetUsers()); + validateBusinessModels(request.getBusinessModels()); + BigDecimal totalCost = validateAndSumCosts(request.getCosts()); + BigDecimal targetPrice = request.getTargetPrice(); + if (targetPrice == null || targetPrice.compareTo(BigDecimal.ZERO) <= 0) throw badRequest("请填写大于 0 的目标售价"); + if (targetPrice.compareTo(totalCost) < 0) throw badRequest("目标售价不能低于单位总成本"); + validateRisks(request.getRisks()); + + BigDecimal unitGrossProfit = targetPrice.subtract(totalCost).setScale(2, RoundingMode.HALF_UP); + BigDecimal grossMargin = unitGrossProfit.multiply(BigDecimal.valueOf(100)).divide(totalCost, 2, RoundingMode.HALF_UP); + BusinessFeasibilityStepOneValidationResult result = new BusinessFeasibilityStepOneValidationResult(); + result.setValid(true); + result.setMessage("校验通过"); + result.setUnitTotalCost(totalCost.setScale(2, RoundingMode.HALF_UP)); + result.setUnitGrossProfit(unitGrossProfit); + result.setGrossMargin(grossMargin); + return result; + } + + private void assertStudent(JwtUser user) { + if (user == null || StringUtils.isBlank(user.getUserId())) throw new ServiceException(HttpStatus.UNAUTHORIZED, "请先登录后再校验"); + if (user.getRoleId() != 4) throw new ServiceException(HttpStatus.FORBIDDEN, "仅学生可进行实训校验"); + } + + private void validateOverview(BusinessFeasibilityStepOneValidationRequest.ProjectOverview overview) { + if (overview == null || isBlank(overview.getProjectBackground()) || isBlank(overview.getProductPositioning()) || isBlank(overview.getProjectGoals()) || isBlank(overview.getMacroEnvironmentConclusion()) || isBlank(overview.getCompetitiveLandscapeConclusion())) throw badRequest("请完整填写项目概述"); + } + + private void validateMarket(BusinessFeasibilityStepOneValidationRequest.MarketAnalysis market) { + if (market == null || invalidPositive(market.getAnnualMarketSize()) || invalidPercentage(market.getMonthlyGrowthRate()) || invalidPercentage(market.getTopBrandMarketShare()) || isBlank(market.getCompetitorsPriceRange()) || invalidPercentage(market.getMarketConcentrationCr3())) throw badRequest("请完整填写市场规模与竞争格局分析"); + } + + private void validateTargetUsers(List users) { + if (users == null || users.size() != 3) throw badRequest("请填写 3 类目标用户群体"); + if (users.stream().anyMatch(user -> user == null || isBlank(user.getUserGroup()) || isBlank(user.getScenario()) || isBlank(user.getCoreNeed()) || isBlank(user.getSpendingPower()))) throw badRequest("请完整填写目标用户群体分析"); + } + + private void validateBusinessModels(List models) { + if (models == null || models.size() != 2) throw badRequest("请填写产品销售渠道和增值服务两项商业模式"); + Set modules = new LinkedHashSet<>(); + for (BusinessFeasibilityStepOneValidationRequest.BusinessModel model : models) { + if (model == null || isBlank(model.getModule()) || isBlank(model.getStrategy()) || isBlank(model.getExplanation()) || !modules.add(StringUtils.trim(model.getModule()))) throw badRequest("请完整填写商业模式梳理"); + } + } + + private BigDecimal validateAndSumCosts(List costs) { + if (costs == null || costs.size() != COST_IDS.size()) throw badRequest("请填写全部成本项目"); + Set submittedIds = new LinkedHashSet<>(); + BigDecimal total = BigDecimal.ZERO; + for (BusinessFeasibilityStepOneValidationRequest.CostItem cost : costs) { + String id = cost == null ? "" : StringUtils.trimToEmpty(cost.getId()); + if (cost == null || !COST_IDS.contains(id) || !submittedIds.add(id) || cost.getAmount() == null || cost.getAmount().compareTo(BigDecimal.ZERO) < 0) throw badRequest("成本项目必须是非负数且不可重复"); + total = total.add(cost.getAmount()); + } + if (!submittedIds.containsAll(COST_IDS) || total.compareTo(BigDecimal.ZERO) <= 0) throw badRequest("单位总成本必须大于 0"); + return total; + } + + private void validateRisks(List risks) { + if (risks == null || risks.size() != RISK_TYPES.size()) throw badRequest("请完整填写 4 类项目风险"); + Set types = new LinkedHashSet<>(); + for (BusinessFeasibilityStepOneValidationRequest.Risk risk : risks) { + String type = risk == null ? "" : StringUtils.trimToEmpty(risk.getType()); + if (!RISK_TYPES.contains(type) || !types.add(type) || isBlank(risk.getDescription()) || !IMPACTS.contains(StringUtils.trimToEmpty(risk.getImpact())) || isBlank(risk.getResponse())) throw badRequest("请完整填写风险描述、影响程度和应对措施"); + } + } + + private boolean invalidPositive(BigDecimal value) { return value == null || value.compareTo(BigDecimal.ZERO) <= 0; } + private boolean invalidPercentage(BigDecimal value) { return value == null || value.compareTo(BigDecimal.ZERO) < 0 || value.compareTo(BigDecimal.valueOf(100)) > 0; } + private boolean isBlank(String value) { return StringUtils.isBlank(value); } + private ServiceException badRequest(String message) { return new ServiceException(HttpStatus.BAD_REQUEST, message); } +} diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/BusinessFeasibilityStepOneServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/BusinessFeasibilityStepOneServiceImplTest.java new file mode 100644 index 0000000..cbc212d --- /dev/null +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/BusinessFeasibilityStepOneServiceImplTest.java @@ -0,0 +1,55 @@ +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.BusinessFeasibilityStepOneValidationRequest; +import com.sztzjy.linkCommerce.entity.dto.BusinessFeasibilityStepOneValidationResult; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BusinessFeasibilityStepOneServiceImplTest { + @Test + void validatesCompletedAnalysisAndRecalculatesProfit() { + BusinessFeasibilityStepOneValidationResult result = new BusinessFeasibilityStepOneServiceImpl().validate(completedRequest(), student()); + assertTrue(result.isValid()); + assertEquals(new BigDecimal("100.00"), result.getUnitTotalCost()); + assertEquals(new BigDecimal("99.00"), result.getUnitGrossProfit()); + assertEquals(new BigDecimal("99.00"), result.getGrossMargin()); + } + + @Test + void rejectsIncompleteRiskAndLossMakingPrice() { + BusinessFeasibilityStepOneValidationRequest incompleteRisk = completedRequest(); + incompleteRisk.getRisks().get(0).setResponse(""); + assertThrows(ServiceException.class, () -> new BusinessFeasibilityStepOneServiceImpl().validate(incompleteRisk, student())); + BusinessFeasibilityStepOneValidationRequest lossMaking = completedRequest(); + lossMaking.setTargetPrice(new BigDecimal("99")); + assertThrows(ServiceException.class, () -> new BusinessFeasibilityStepOneServiceImpl().validate(lossMaking, student())); + } + + private BusinessFeasibilityStepOneValidationRequest completedRequest() { + BusinessFeasibilityStepOneValidationRequest request = new BusinessFeasibilityStepOneValidationRequest(); + BusinessFeasibilityStepOneValidationRequest.ProjectOverview overview = new BusinessFeasibilityStepOneValidationRequest.ProjectOverview(); + overview.setProjectBackground("背景"); overview.setProductPositioning("定位"); overview.setProjectGoals("目标"); overview.setMacroEnvironmentConclusion("PEST 结论"); overview.setCompetitiveLandscapeConclusion("SWOT 结论"); request.setProjectOverview(overview); + BusinessFeasibilityStepOneValidationRequest.MarketAnalysis market = new BusinessFeasibilityStepOneValidationRequest.MarketAnalysis(); + market.setAnnualMarketSize(new BigDecimal("12")); market.setMonthlyGrowthRate(new BigDecimal("3")); market.setTopBrandMarketShare(new BigDecimal("28")); market.setCompetitorsPriceRange("199-499"); market.setMarketConcentrationCr3(new BigDecimal("55")); request.setMarketAnalysis(market); + request.setTargetUsers(Arrays.asList(targetUser(), targetUser(), targetUser())); + request.setBusinessModels(Arrays.asList(model("产品销售渠道"), model("增值服务"))); + ArrayList costs = new ArrayList<>(); + Arrays.asList("hardware-bom", "manufacturing", "rd-amortization", "channel-marketing", "logistics-warehouse").forEach(id -> { BusinessFeasibilityStepOneValidationRequest.CostItem item = new BusinessFeasibilityStepOneValidationRequest.CostItem(); item.setId(id); item.setAmount(new BigDecimal("20")); costs.add(item); }); + request.setCosts(costs); request.setTargetPrice(new BigDecimal("199")); + request.setRisks(Arrays.asList(risk("技术风险"), risk("市场风险"), risk("供应链风险"), risk("资金风险"))); + return request; + } + private BusinessFeasibilityStepOneValidationRequest.TargetUser targetUser() { BusinessFeasibilityStepOneValidationRequest.TargetUser value = new BusinessFeasibilityStepOneValidationRequest.TargetUser(); value.setUserGroup("用户"); value.setScenario("场景"); value.setCoreNeed("需求"); value.setSpendingPower("中"); return value; } + private BusinessFeasibilityStepOneValidationRequest.BusinessModel model(String module) { BusinessFeasibilityStepOneValidationRequest.BusinessModel value = new BusinessFeasibilityStepOneValidationRequest.BusinessModel(); value.setModule(module); value.setStrategy("策略"); value.setExplanation("说明"); return value; } + private BusinessFeasibilityStepOneValidationRequest.Risk risk(String type) { BusinessFeasibilityStepOneValidationRequest.Risk value = new BusinessFeasibilityStepOneValidationRequest.Risk(); value.setType(type); value.setDescription("风险描述"); value.setImpact("高"); value.setResponse("应对措施"); return value; } + private JwtUser student() { JwtUser user = new JwtUser(); user.setUserId("stu-1"); user.setRoleId(4); return user; } +}