feat: validate business feasibility step one
parent
c47616b1ec
commit
90956084a5
@ -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…
Reference in New Issue