feat: validate business feasibility step one

main
chenyuan 3 weeks ago
parent c47616b1ec
commit 90956084a5

@ -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<StudentTrainingAnswer> 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<BusinessFeasibilityStepOneValidationResult> 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,

@ -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<TargetUser> targetUsers;
private List<BusinessModel> businessModels;
private List<CostItem> costs;
private BigDecimal targetPrice;
private List<Risk> 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<TargetUser> getTargetUsers() { return targetUsers; }
public void setTargetUsers(List<TargetUser> targetUsers) { this.targetUsers = targetUsers; }
public List<BusinessModel> getBusinessModels() { return businessModels; }
public void setBusinessModels(List<BusinessModel> businessModels) { this.businessModels = businessModels; }
public List<CostItem> getCosts() { return costs; }
public void setCosts(List<CostItem> costs) { this.costs = costs; }
public BigDecimal getTargetPrice() { return targetPrice; }
public void setTargetPrice(BigDecimal targetPrice) { this.targetPrice = targetPrice; }
public List<Risk> getRisks() { return risks; }
public void setRisks(List<Risk> 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; }
}
}

@ -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; }
}

@ -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);
}

@ -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<String> COST_IDS = Arrays.asList("hardware-bom", "manufacturing", "rd-amortization", "channel-marketing", "logistics-warehouse");
private static final List<String> RISK_TYPES = Arrays.asList("技术风险", "市场风险", "供应链风险", "资金风险");
private static final Set<String> 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<BusinessFeasibilityStepOneValidationRequest.TargetUser> 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<BusinessFeasibilityStepOneValidationRequest.BusinessModel> models) {
if (models == null || models.size() != 2) throw badRequest("请填写产品销售渠道和增值服务两项商业模式");
Set<String> 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<BusinessFeasibilityStepOneValidationRequest.CostItem> costs) {
if (costs == null || costs.size() != COST_IDS.size()) throw badRequest("请填写全部成本项目");
Set<String> 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<BusinessFeasibilityStepOneValidationRequest.Risk> risks) {
if (risks == null || risks.size() != RISK_TYPES.size()) throw badRequest("请完整填写 4 类项目风险");
Set<String> 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); }
}

@ -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<BusinessFeasibilityStepOneValidationRequest.CostItem> 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; }
}
Loading…
Cancel
Save