You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/api/studentTrainingAnswer.js

2022 lines
117 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import request from "@/utils/request";
import { getDemoAnswerKey, getStudentDemoSession, isStudentDemo } from "@/utils/studentDemo";
const newProductSurveyStepOneAnswers = {
"selling-point-1": "核心层",
"selling-point-2": "有形层",
"selling-point-3": "核心层",
"selling-point-4": "核心层",
"selling-point-5": "有形层",
"selling-point-6": "有形层",
"selling-point-7": "有形层",
"selling-point-8": "延伸层",
"selling-point-9": "延伸层",
"selling-point-10": "延伸层",
"selling-point-11": "有形层",
};
function demoResult(taskKey) {
try { return JSON.parse(sessionStorage.getItem(getDemoAnswerKey(taskKey)) || "null"); } catch (error) { return null; }
}
export function getStudentTrainingAnswer(taskKey, params) {
if (isStudentDemo()) return Promise.resolve({ code: 200, data: demoResult(taskKey) });
return request({
url: `/api/student-training-answers/${taskKey}`,
method: "get",
params,
});
}
export function saveStudentTrainingAnswer(taskKey, data) {
if (isStudentDemo()) {
const answer = { ...(demoResult(taskKey) || {}), ...data, teachingClassId: getStudentDemoSession()?.userInfo?.demoTeachingClassId };
sessionStorage.setItem(getDemoAnswerKey(taskKey), JSON.stringify(answer));
return Promise.resolve({ code: 200, data: answer });
}
return request({
url: `/api/student-training-answers/${taskKey}`,
method: "post",
data,
headers: { repeatSubmit: false },
});
}
export function deleteStudentTrainingAnswer(taskKey, params) {
if (isStudentDemo()) { sessionStorage.removeItem(getDemoAnswerKey(taskKey)); return Promise.resolve({ code: 200 }); }
return request({
url: `/api/student-training-answers/${taskKey}`,
method: "delete",
params,
});
}
export function uploadStudentTrainingFile(data) {
if (isStudentDemo()) return Promise.reject(new Error("演示模式不上传文件"));
return request({
url: "/common/upload",
method: "post",
data,
headers: {
"Content-Type": "multipart/form-data",
repeatSubmit: false,
},
});
}
export function validateProductPricingStepOne(payload) {
const value = payload || {};
const rows = Array.isArray(value.pricingRows) ? value.pricingRows : [];
if (isStudentDemo()) {
const ranges = { dealer: [15, 25], "direct-retail": [40, 60], "premium-brand": [70, 80] };
const unitCost = Number(value.unitCost);
if (!Number.isFinite(unitCost) || unitCost <= 0) return Promise.reject(new Error("单台综合成本必须大于 0"));
if (rows.length !== 3) return Promise.reject(new Error("请填写三个渠道的加成率"));
try {
const pricingRows = Object.keys(ranges).map((id) => {
const markupRate = Number(rows.find((item) => item?.id === id)?.markupRate);
const [min, max] = ranges[id];
if (!Number.isFinite(markupRate) || markupRate < min || markupRate > max) {
throw new Error(`渠道加成率需在 ${min}%~${max}% 范围内`);
}
return { id, markupRate: Number(markupRate.toFixed(2)), salePrice: Number((unitCost * (1 + markupRate / 100)).toFixed(2)) };
});
return Promise.resolve({ code: 200, data: { valid: true, message: "渠道加成定价已校验", unitCost: Number(unitCost.toFixed(2)), pricingRows } });
} catch (error) {
return Promise.reject(error);
}
}
return request({ url: "/api/student-training-answers/product-pricing/step-1/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function validateProductPricingStepTwo(payload) {
const value = payload || {};
const rows = Array.isArray(value.competitorRows) ? value.competitorRows : [];
const decision = value.decision || {};
if (isStudentDemo()) {
const unitCost = Number(value.unitCost);
if (!Number.isFinite(unitCost) || unitCost <= 0) return Promise.reject(new Error("请先完成第 1 步并填写单台综合成本"));
if (rows.length !== 4) return Promise.reject(new Error("请完整填写 4 个竞品信息"));
try {
const productTypes = rows.map((row, index) => {
const productType = String(row?.productType || "").trim();
const brand = String(row?.brand || "").trim();
const differentiator = String(row?.differentiator || "").trim();
const ecommercePrice = Number(row?.ecommercePrice);
if (!productType || !brand || !differentiator || !Number.isFinite(ecommercePrice) || ecommercePrice <= 0) throw new Error(`请完整填写竞品 ${index + 1} 的信息`);
return productType;
});
if (new Set(productTypes).size !== productTypes.length) {
throw new Error("竞品产品类型不能重复,请使用可区分的竞品名称");
}
const targetPrice = Number(decision.targetPrice);
if (!productTypes.includes(String(decision.benchmarkCompetitor || "").trim())) throw new Error("对标竞品必须从上方已填写的竞品中选择");
if (!Number.isFinite(targetPrice) || targetPrice <= 0 || !String(decision.differentiator || "").trim() || !String(decision.pricingReason || "").trim()) throw new Error("请完整填写本产品定价、差异化优势和定价原因");
return Promise.resolve({ code: 200, data: { valid: true, message: "竞品价格带与定价已校验", competitorRows: rows, decision: { ...decision, targetPrice: Number(targetPrice.toFixed(2)), grossMarginRate: Number((((targetPrice - unitCost) / targetPrice) * 100).toFixed(2)) } } });
} catch (error) {
return Promise.reject(error);
}
}
return request({ url: "/api/student-training-answers/product-pricing/step-2/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function validateProductPricingStepThree(payload) {
const value = payload || {};
const valueItems = Array.isArray(value.valueItems) ? value.valueItems : [];
if (isStudentDemo()) {
try {
if (!valueItems.length) throw new Error("请至少填写一项可替代产品或功能的价值感知");
const normalizedItems = valueItems.map((item, index) => {
const name = String(item?.name || "").trim();
const perceivedValue = Number(item?.perceivedValue);
if (!String(item?.id || "").trim() || !name || !Number.isFinite(perceivedValue) || perceivedValue <= 0) {
throw new Error(`请完整填写第 ${index + 1} 项价值感知`);
}
return { ...item, id: String(item.id).trim(), name, perceivedValue: Number(perceivedValue.toFixed(2)) };
});
if (new Set(normalizedItems.map((item) => item.id)).size !== normalizedItems.length) throw new Error("价值感知条目不能重复");
const discountRate = Number(value.discountRate);
if (!Number.isFinite(discountRate) || discountRate <= 0 || discountRate > 100) throw new Error("利润让渡折扣率应大于 0 且不超过 100%");
const perceivedValueTotal = Number(normalizedItems.reduce((total, item) => total + item.perceivedValue, 0).toFixed(2));
return Promise.resolve({
code: 200,
data: {
valid: true,
message: "用户感知价值定价已校验",
valueItems: normalizedItems,
perceivedValueTotal,
discountRate: Number(discountRate.toFixed(2)),
valueBasedPrice: Number((perceivedValueTotal * discountRate / 100).toFixed(2)),
},
});
} catch (error) {
return Promise.reject(error);
}
}
return request({ url: "/api/student-training-answers/product-pricing/step-3/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function validateProductPricingStepFour(payload) {
const value = payload || {};
const psmRows = Array.isArray(value.psmRows) ? value.psmRows : [];
const pricePoints = value.pricePoints || {};
if (isStudentDemo()) {
try {
const requiredRanges = ["≤100元", "100~150元", "150~200元", "200~250元", "250~300元", "≥300元"];
if (psmRows.length < requiredRanges.length) throw new Error("请完整填写 6 个固定价格区间的 PSM 累计占比");
const seenIds = new Set();
const seenRanges = new Set();
const normalizedRows = psmRows.map((row, index) => {
const id = String(row?.id || "").trim();
const priceRange = String(row?.priceRange || "").trim();
if (!id || !priceRange || seenIds.has(id) || seenRanges.has(priceRange)) throw new Error("PSM 价格区间不能重复且不能为空");
seenIds.add(id);
seenRanges.add(priceRange);
const normalized = { ...row, id, priceRange };
[["tooExpensive", "太贵不买"], ["tooCheap", "太便宜不买"], ["expensiveAcceptable", "偏高可接受"], ["cheapValue", "便宜划算"]].forEach(([field, label]) => {
const percentage = Number(row?.[field]);
if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) throw new Error(`第 ${index + 1} 行“${label}”累计占比应在 0%–100% 之间`);
normalized[field] = Number(percentage.toFixed(2));
});
return normalized;
});
if (!requiredRanges.every((range) => seenRanges.has(range))) throw new Error("请保留并填写 6 个固定价格区间");
const optimalPrice = Number(pricePoints.optimalPrice);
const acceptablePrice = Number(pricePoints.acceptablePrice);
const acceptableRange = String(pricePoints.acceptableRange || "").trim();
if (!Number.isFinite(optimalPrice) || optimalPrice <= 0 || !Number.isFinite(acceptablePrice) || acceptablePrice <= 0 || !acceptableRange) {
throw new Error("请完整填写最优价格点、可接受价格点和可接受价格范围");
}
return Promise.resolve({ code: 200, data: {
valid: true,
message: "PSM 价格敏感度测试已校验",
psmRows: normalizedRows,
pricePoints: { optimalPrice: Number(optimalPrice.toFixed(2)), acceptablePrice: Number(acceptablePrice.toFixed(2)), acceptableRange },
} });
} catch (error) {
return Promise.reject(error);
}
}
return request({ url: "/api/student-training-answers/product-pricing/step-4/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function validateProductPricingStepFive(payload) {
const value = payload || {};
const methodResults = Array.isArray(value.methodResults) ? value.methodResults : [];
if (isStudentDemo()) {
try {
const requiredKeys = ["cost-markup", "competitive", "value-based", "psm"];
const selectedCostMarkupChannel = String(value.selectedCostMarkupChannel || "").trim();
if (!selectedCostMarkupChannel) throw new Error("请选择用于汇总的成本加成定价渠道");
if (methodResults.length !== requiredKeys.length) throw new Error("请完整汇总四种定价方法的测算结果");
const seenKeys = new Set();
const normalizedResults = methodResults.map((item) => {
const key = String(item?.key || "").trim();
const methodName = String(item?.methodName || "").trim();
const measuredPrice = Number(item?.measuredPrice);
const grossMarginRate = Number(item?.grossMarginRate);
if (!requiredKeys.includes(key) || seenKeys.has(key) || !methodName || !Number.isFinite(measuredPrice) || measuredPrice <= 0 || !Number.isFinite(grossMarginRate)) {
throw new Error("请完整填写四种定价方法的测算结果和毛利润率");
}
if (grossMarginRate < -100 || grossMarginRate > 100) throw new Error("毛利润率应在 -100% 至 100% 之间");
seenKeys.add(key);
return { key, methodName, measuredPrice: Number(measuredPrice.toFixed(2)), grossMarginRate: Number(grossMarginRate.toFixed(2)) };
});
if (!requiredKeys.every((key) => seenKeys.has(key))) throw new Error("定价方法汇总项不正确或重复");
const finalAnalysis = String(value.finalAnalysis || "").trim();
if (!finalAnalysis) throw new Error("请填写最终量产定价分析");
return Promise.resolve({ code: 200, data: { valid: true, message: "最终量产定价汇总已校验", selectedCostMarkupChannel, methodResults: normalizedResults, finalAnalysis } });
} catch (error) {
return Promise.reject(error);
}
}
return request({ url: "/api/student-training-answers/product-pricing/step-5/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function checkNewProductSurveyStepOne(items) {
if (isStudentDemo()) {
const results = Array.isArray(items)
? items.map((item) => {
const correct = newProductSurveyStepOneAnswers[item?.id] === item?.category;
return {
id: item?.id,
correct,
message: correct ? "分类正确" : "分类有误,请重新选择",
};
})
: [];
const correctCount = results.filter((item) => item.correct).length;
return Promise.resolve({
code: 200,
data: {
items: results,
correctCount,
totalCount: results.length,
allCorrect: results.length === Object.keys(newProductSurveyStepOneAnswers).length && correctCount === results.length,
},
});
}
return request({
url: "/api/student-training-answers/new-product-survey/step-1/check",
method: "post",
data: { items },
headers: { repeatSubmit: false },
});
}
export function validateReleaseChannelEvaluationStepOne(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
const normalizedRows = rows.map((row) => ({
dimension: String(row?.dimension || "").trim(),
description: String(row?.description || "").trim(),
weight: Number(row?.weight),
}));
const totalWeight = Number(normalizedRows.reduce((sum, row) => sum + row.weight, 0).toFixed(6));
if (!normalizedRows.length || normalizedRows.some((row) => !row.dimension || !row.description
|| !Number.isFinite(row.weight) || row.weight < 0) || totalWeight !== 100) {
return Promise.reject(new Error("请填写每行评估维度、维度说明和非负权重,且权重之和必须等于100%"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "权重校验通过", totalWeight } });
}
return request({
url: "/api/student-training-answers/release-channel-evaluation/step-1/validate",
method: "post",
data: { rows },
headers: { repeatSubmit: false },
});
}
export function validateReleaseChannelEvaluationStepTwo(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
const normalizedRows = rows.map((row) => ({
channel: String(row?.channel || "").trim(),
dailySearchVolume: Number(row?.dailySearchVolume),
supplyDemandRatio: Number(row?.supplyDemandRatio),
averageClickRate: Number(row?.averageClickRate),
averageCpc: Number(row?.averageCpc),
averageConversionRate: Number(row?.averageConversionRate),
existingVisitorCount: Number(row?.existingVisitorCount),
matchingScore: Number(row?.matchingScore),
}));
const valid = normalizedRows.length > 0 && normalizedRows.every((row) => row.channel
&& [row.dailySearchVolume, row.supplyDemandRatio, row.averageCpc, row.existingVisitorCount]
.every((value) => Number.isFinite(value) && value >= 0)
&& [row.averageClickRate, row.averageConversionRate].every((value) => Number.isFinite(value) && value >= 0 && value <= 100)
&& Number.isInteger(row.matchingScore) && row.matchingScore >= 1 && row.matchingScore <= 5);
if (!valid) return Promise.reject(new Error("请完整填写渠道数据;点击率和转化率为0-100%,匹配度为1-5分"));
return Promise.resolve({ code: 200, data: { valid: true, message: "渠道数据校验通过", rowCount: normalizedRows.length } });
}
return request({
url: "/api/student-training-answers/release-channel-evaluation/step-2/validate",
method: "post",
data: { rows },
headers: { repeatSubmit: false },
});
}
export function validateReleaseChannelEvaluationStepThree(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
try {
const saved = demoResult("release-channel-evaluation") || {};
const stepOne = typeof saved.step1Answer === "string" ? JSON.parse(saved.step1Answer || "{}") : saved.step1Answer || {};
const stepTwo = typeof saved.step2Answer === "string" ? JSON.parse(saved.step2Answer || "{}") : saved.step2Answer || {};
const sourceRows = Array.isArray(stepTwo.rows) ? stepTwo.rows : [];
const weights = Object.fromEntries((Array.isArray(stepOne.rows) ? stepOne.rows : []).map((row) => [row?.dimension, Number(row?.weight)]));
const metrics = [
["市场规模潜力", "dailySearchVolume", false], ["竞争激烈程度", "supplyDemandRatio", true],
["流量点击效率", "averageClickRate", false], ["流量获取成本", "averageCpc", true],
["流量转化效率", "averageConversionRate", false], ["现有用户基础", "existingVisitorCount", false], ["匹配度", "matchingScore", false],
];
if (!sourceRows.length || rows.length !== sourceRows.length) throw new Error("请先完成第二步全部渠道数据");
const submitted = new Map(rows.map((row) => [String(row?.channel || "").trim(), row]));
for (const source of sourceRows) {
const channel = String(source?.channel || "").trim();
const row = submitted.get(channel);
if (!channel || !row) throw new Error("第三步渠道必须与第二步保持一致");
const dimensions = new Map((Array.isArray(row.dimensions) ? row.dimensions : []).map((item) => [item?.dimension, item]));
let expectedTotal = 0;
metrics.forEach(([dimension, field, reverse]) => {
const weight = Number(weights[dimension]);
const values = sourceRows.map((item) => Number(item?.[field]));
const value = Number(source?.[field]);
const min = Math.min(...values);
const max = Math.max(...values);
const expectedScore = max === min ? 10 : Number((((reverse ? max - value : value - min) * 10) / (max - min)).toFixed(1));
const supplied = dimensions.get(dimension);
if (!supplied || Number(supplied.weight) !== weight || Number(supplied.score) !== expectedScore) throw new Error(`“${channel}”的“${dimension}”得分或权重不正确`);
expectedTotal += expectedScore * weight / 100;
});
if (Number(row.weightedScore) !== Number(expectedTotal.toFixed(2))) throw new Error(`“${channel}”的加权得分不正确`);
}
return Promise.resolve({ code: 200, data: { valid: true, message: "归一化得分与加权得分校验通过", rowCount: rows.length } });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/release-channel-evaluation/step-3/validate",
method: "post",
data: { rows },
headers: { repeatSubmit: false },
});
}
export function validateReleaseChannelEvaluationStepFour(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
try {
const analysis = String(payload?.coreChannelAnalysis || "").trim();
const saved = demoResult("release-channel-evaluation") || {};
const stepThree = typeof saved.step3Answer === "string" ? JSON.parse(saved.step3Answer || "{}") : saved.step3Answer || {};
const expectedRows = Array.isArray(stepThree.rows) ? stepThree.rows : [];
if (!analysis) throw new Error("请分析核心渠道优势并填写运营建议");
if (!expectedRows.length || rows.length !== expectedRows.length) throw new Error("渠道数量必须与第三步一致");
const expected = new Map(expectedRows.map((row) => [String(row?.channel || "").trim(), Number(row?.weightedScore)]));
const channels = new Set();
let previousScore = null;
rows.forEach((row, index) => {
const channel = String(row?.channel || "").trim();
const score = Number(row?.weightedScore);
if (!channel || channels.has(channel) || !expected.has(channel)) throw new Error(`第${index + 1}行渠道必须与第三步一致且不能重复`);
if (!Number.isFinite(score) || score !== expected.get(channel)) throw new Error(`“${channel}”的加权综合得分必须与第三步一致`);
if (previousScore !== null && previousScore < score) throw new Error("加权综合得分必须按从高到低顺序填写");
channels.add(channel);
previousScore = score;
});
return Promise.resolve({ code: 200, data: { valid: true, message: "渠道排名与核心渠道校验通过", coreChannels: rows.slice(0, 2).map((row) => row.channel) } });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/release-channel-evaluation/step-4/validate",
method: "post",
data: { rows, coreChannelAnalysis: payload?.coreChannelAnalysis },
headers: { repeatSubmit: false },
});
}
export function validateProductDiagnosisIterationStepOne(payload) {
const value = payload || {};
const conversionRates = Array.isArray(value.conversionRates) ? value.conversionRates : [];
const anomalyRows = Array.isArray(value.anomalyRows) ? value.anomalyRows : [];
if (isStudentDemo()) {
try {
const nodes = ["曝光-点击", "点击-加购", "加购-下单", "下单-好评", "好评-复购"];
const channels = ["天猫", "京东", "抖音", "拼多多"];
if (conversionRates.length !== nodes.length || new Set(conversionRates.map((row) => String(row?.linkNode || "").trim())).size !== nodes.length) {
throw new Error("请完整填写 5 个链路节点的转化率");
}
const rates = new Map();
conversionRates.forEach((row) => {
const node = String(row?.linkNode || "").trim();
if (!nodes.includes(node)) throw new Error("链路节点必须与题目中的 5 个节点一致");
const values = [row?.tmall, row?.jd, row?.douyin, row?.pinduoduo, row?.industryAverage].map(Number);
if (values.some((item) => !Number.isFinite(item) || item < 0 || item > 100)) throw new Error("请填写 0 至 100 之间的转化率");
rates.set(node, { ...row, values });
});
if (!String(value.funnelChartUrl || "").trim()) throw new Error("请上传节点转化漏斗对比图");
if (anomalyRows.length < 1 || anomalyRows.length > 5) throw new Error("异常节点分析最少填写 1 行,最多填写 5 行");
anomalyRows.forEach((row, index) => {
const channel = String(row?.channel || "").trim();
const node = String(row?.linkNode || "").trim();
const source = rates.get(node);
if (!channels.includes(channel)) throw new Error(`第 ${index + 1} 行渠道必须来源于第一个表格`);
if (!source) throw new Error(`第 ${index + 1} 行链路节点必须来源于第一个表格`);
const sourceRate = source[{ 天猫: "tmall", 京东: "jd", 抖音: "douyin", 拼多多: "pinduoduo" }[channel]];
if (Number(row?.storeConversionRate) !== Number(sourceRate) || Number(row?.industryAverageConversionRate) !== Number(source.industryAverage)) {
throw new Error(`第 ${index + 1} 行的店铺和行业转化率必须与第一个表格对应数据一致`);
}
if (!String(row?.anomalyAnalysis || "").trim()) throw new Error(`请填写第 ${index + 1} 行的异常分析与判定`);
});
return Promise.resolve({ code: 200, data: { valid: true, message: "转化率、漏斗图和异常节点分析校验通过" } });
} catch (error) { return Promise.reject(error); }
}
return request({
url: "/api/student-training-answers/product-diagnosis-iteration/step-1/validate",
method: "post",
data: { conversionRates, funnelChartUrl: value.funnelChartUrl, anomalyRows },
headers: { repeatSubmit: false },
});
}
export async function analyzeProductDiagnosisIterationStepTwo(file) {
if (isStudentDemo()) {
const XLSX = await import("xlsx");
const buffer = await file.arrayBuffer();
const name = String(file?.name || "评论数据");
let comments = [];
if (/\.xlsx?$/i.test(name)) {
const workbook = XLSX.read(buffer, { type: "array" });
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { header: 1, defval: "" });
comments = rows.slice(1).map((row) => row.map((item) => String(item || "").trim()).filter((item) => /[\u4e00-\u9fa5A-Za-z]/.test(item)).sort((a, b) => b.length - a.length)[0] || "").filter(Boolean);
} else comments = new TextDecoder("utf-8").decode(buffer).split(/\r?\n/).slice(1).map((line) => line.split(/[,,\t]/).map((item) => item.trim()).sort((a, b) => b.length - a.length)[0] || "").filter(Boolean);
const positiveWords = ["很好", "喜欢", "满意", "推荐", "好用", "值得", "清晰", "漂亮", "方便", "优秀", "赞"];
const negativeWords = ["太慢", "差", "坏", "问题", "失望", "退货", "卡顿", "慢", "漏水", "故障", "不行", "不值", "异味"];
const issueWords = ["质量", "物流", "故障", "外观", "价格", "包装", "发货", "售后", "退货", "卡顿", "漏水", "安装", "尺寸", "噪音", "异味", "续航", "慢"];
const count = (text, words) => words.filter((word) => text.includes(word)).length;
const frequencies = new Map(); let positiveCount = 0; let neutralCount = 0; let negativeCount = 0;
comments.forEach((comment) => { const positive = count(comment, positiveWords); const negative = count(comment, negativeWords); if (positive > negative) positiveCount += 1; else if (negative > positive) negativeCount += 1; else neutralCount += 1; issueWords.forEach((word) => { if (comment.includes(word)) frequencies.set(word, (frequencies.get(word) || 0) + 1); }); });
const wordCloud = [...frequencies.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], "zh-CN")).slice(0, 30).map(([name, value]) => ({ name, value }));
if (!comments.length) return Promise.reject(new Error("未从文件中读取到有效评论内容"));
return { code: 200, data: { sourceFileName: name, totalComments: comments.length, positiveCount, neutralCount, negativeCount, topIssues: wordCloud.slice(0, 3).map((item) => item.name), wordCloud, previewComments: comments.slice(0, 5) } };
}
const data = new FormData(); data.append("file", file);
return request({ url: "/api/student-training-answers/product-diagnosis-iteration/step-2/analyze", method: "post", data, headers: { "Content-Type": "multipart/form-data", repeatSubmit: false } });
}
export function validateProductDiagnosisIterationStepTwo(payload) {
const value = payload || {}; const sentiments = Array.isArray(value.sentiments) ? value.sentiments : [];
if (isStudentDemo()) {
try {
const topIssues = Array.isArray(value.topIssues) ? value.topIssues.map((item) => String(item || "").trim()) : [];
if (!String(value.sourceFileName || "").trim()) throw new Error("请先导入评论数据并生成词云");
if (topIssues.length !== 3 || topIssues.some((item) => !item) || new Set(topIssues).size !== 3) throw new Error("请填写词云中出现频次最高的 3 个不重复问题");
if (!String(value.rootCauseAnalysis || "").trim()) throw new Error("请结合词云和情感分析填写核心问题根本原因");
if (sentiments.length !== 3) throw new Error("请填写正面、中性、负面三类情感数据");
const total = sentiments.reduce((sum, item) => sum + Number(item?.count), 0);
if (!Number.isFinite(total) || total <= 0) throw new Error("情感评论数量合计必须大于 0");
sentiments.forEach((item) => { const expected = Number((Number(item?.count) * 100 / total).toFixed(2)); if (!Number.isFinite(Number(item?.count)) || Number(item?.count) < 0 || Number(item?.percentage) !== expected) throw new Error(`${item?.type || "情感"}评论占比计算不正确`); });
return Promise.resolve({ code: 200, data: { valid: true, message: "情感评论占比计算校验通过" } });
} catch (error) { return Promise.reject(error); }
}
return request({ url: "/api/student-training-answers/product-diagnosis-iteration/step-2/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function validateProductDiagnosisIterationStepThree(payload) {
const value = payload || {}; const rows = Array.isArray(value.rows) ? value.rows : [];
if (isStudentDemo()) {
try {
const metrics = ["点击-加购转化率", "加购-下单转化率", "下单-好评转化率", "详情页跳出率", "退货率"];
if (!String(value.testPlan || "").trim()) throw new Error("请填写 AB 测试方案");
if (!String(value.comparisonChartUrl || "").trim()) throw new Error("请上传 AB 测试结果对比图");
if (!String(value.iterationRecommendation || "").trim()) throw new Error("请填写诊断结论与迭代优化建议");
if (rows.length !== metrics.length || new Set(rows.map((row) => String(row?.metric || "").trim())).size !== metrics.length) throw new Error("请完整填写 5 项 AB 测试指标");
rows.forEach((row) => { const control = Number(row?.controlValue); const experiment = Number(row?.experimentValue); if (!metrics.includes(String(row?.metric || "").trim()) || !Number.isFinite(control) || !Number.isFinite(experiment) || control < 0 || control > 100 || experiment < 0 || experiment > 100 || !String(row?.analysis || "").trim()) throw new Error("请完整填写五项指标的对照组、实验组数据和判定分析"); });
return Promise.resolve({ code: 200, data: { valid: true, message: "AB 测试方案、数据与对比图校验通过" } });
} catch (error) { return Promise.reject(error); }
}
return request({ url: "/api/student-training-answers/product-diagnosis-iteration/step-3/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function validatePromotionEffectOptimizationStepOne(payload) {
const value = payload || {};
const rows = Array.isArray(value.rows) ? value.rows : [];
if (isStudentDemo()) {
try {
const totalBudget = Number(value.totalBudget);
if (!Number.isFinite(totalBudget) || totalBudget <= 0) throw new Error("请填写大于 0 的新产品投放试错预算");
if (rows.length < 2 || rows.length > 5) throw new Error("请填写 2 至 5 个投放渠道");
const saved = demoResult("release-channel-evaluation") || {};
const stepTwo = typeof saved.step2Answer === "string" ? JSON.parse(saved.step2Answer || "{}") : saved.step2Answer || {};
const availableChannels = new Set((Array.isArray(stepTwo.rows) ? stepTwo.rows : [])
.map((row) => String(row?.channel || "").trim()).filter(Boolean));
if (!availableChannels.size) throw new Error("请先完成产品发布渠道评估中的渠道数据填写");
const selected = new Set();
let totalPercentage = 0;
rows.forEach((row, index) => {
const channel = String(row?.channel || "").trim();
const positioning = String(row?.channelPositioning || "").trim();
const percentage = Number(row?.budgetPercentage);
const amount = Number(row?.budgetAmount);
const phases = [row?.coldStartBudget, row?.growthBudget, row?.stableBudget].map(Number);
if (!availableChannels.has(channel) || selected.has(channel)) throw new Error(`第 ${index + 1} 行渠道必须来自产品发布渠道评估,且不能重复`);
if (!positioning) throw new Error(`请选择第 ${index + 1} 行的渠道定位`);
if (!Number.isFinite(percentage) || percentage < 0) throw new Error(`请填写第 ${index + 1} 行的非负预算占比`);
const expectedAmount = Number((totalBudget * percentage / 100).toFixed(2));
if (!Number.isFinite(amount) || Number(amount.toFixed(2)) !== expectedAmount) throw new Error(`第 ${index + 1} 行预算金额应由试错预算和预算占比自动计算`);
if (phases.some((item) => !Number.isFinite(item) || item < 0) || Number(phases.reduce((sum, item) => sum + item, 0).toFixed(2)) !== expectedAmount) {
throw new Error(`第 ${index + 1} 行三个阶段投放预算之和必须等于预算金额`);
}
selected.add(channel);
totalPercentage += percentage;
});
if (Number(totalPercentage.toFixed(6)) !== 100) throw new Error("各渠道预算占比之和必须等于 100%");
return Promise.resolve({ code: 200, data: { valid: true, message: "投放预算分配校验通过", totalBudget: Number(totalBudget.toFixed(2)), availableChannels: [...availableChannels] } });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/promotion-effect-optimization/step-1/validate",
method: "post",
data: { totalBudget: value.totalBudget, rows },
headers: { repeatSubmit: false },
});
}
export function validatePromotionEffectOptimizationStepTwo(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
try {
const saved = demoResult("promotion-effect-optimization") || {};
const stepOne = typeof saved.step1Answer === "string" ? JSON.parse(saved.step1Answer || "{}") : saved.step1Answer || {};
const channels = [...new Set((Array.isArray(stepOne.rows) ? stepOne.rows : [])
.map((row) => String(row?.channel || "").trim()).filter(Boolean))];
const phases = ["冷启动期", "增长期", "稳定期"];
if (channels.length < 2 || channels.length > 5) throw new Error("请先完成并保存第一步投放预算分配");
if (rows.length !== channels.length * phases.length) throw new Error("请为每个渠道的冷启动期、增长期和稳定期分别填写两项投放策略");
const cells = new Set();
rows.forEach((row) => {
const phase = String(row?.phase || "").trim();
const channel = String(row?.channel || "").trim();
const strategies = Array.isArray(row?.strategies) ? row.strategies.map((item) => String(item || "").trim()) : [];
if (!phases.includes(phase) || !channels.includes(channel)) throw new Error("投放策略的阶段和渠道必须与第一步保持一致");
const cell = `${phase}\u0000${channel}`;
if (cells.has(cell)) throw new Error("每个阶段和渠道只能填写一组投放策略");
if (strategies.length !== 2 || strategies.some((item) => !item) || new Set(strategies).size !== 2) {
throw new Error(`“${phase} - ${channel}”必须填写两项不重复的投放策略`);
}
cells.add(cell);
});
return Promise.resolve({ code: 200, data: { valid: true, message: "各阶段投放策略校验通过", channelCount: channels.length, cellCount: cells.size } });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/promotion-effect-optimization/step-2/validate",
method: "post",
data: { rows },
headers: { repeatSubmit: false },
});
}
export function validatePromotionEffectOptimizationStepThree(payload) {
const value = payload || {};
const rows = Array.isArray(value.rows) ? value.rows : [];
if (isStudentDemo()) {
try {
const grossMargin = Number(value.grossMargin);
const breakEvenRoi = Number(value.breakEvenRoi);
if (!Number.isFinite(grossMargin) || grossMargin <= 0 || grossMargin > 100) throw new Error("请填写大于 0 且不超过 100% 的产品毛利率");
if (breakEvenRoi !== Number((100 / grossMargin).toFixed(2))) throw new Error("保本 ROI 应等于 1 ÷ 毛利率,结果保留两位小数");
const saved = demoResult("promotion-effect-optimization") || {};
const stepOne = typeof saved.step1Answer === "string" ? JSON.parse(saved.step1Answer || "{}") : saved.step1Answer || {};
const channels = [...new Set((Array.isArray(stepOne.rows) ? stepOne.rows : []).map((row) => String(row?.channel || "").trim()).filter(Boolean))];
if (rows.length !== channels.length + 1) throw new Error("请填写所有渠道数据,并在最后一行填写合计");
const channelRows = rows.slice(0, -1);
const sum = { impressions: 0, clicks: 0, turnover: 0, promotionCost: 0 };
const validateRow = (row, label) => {
const values = ["impressions", "clicks", "turnover", "promotionCost", "clickRate", "actualRoi"].reduce((all, key) => ({ ...all, [key]: Number(row?.[key]) }), {});
if (!Number.isFinite(values.impressions) || values.impressions <= 0 || !Number.isFinite(values.clicks) || values.clicks < 0 || !Number.isFinite(values.turnover) || values.turnover < 0 || !Number.isFinite(values.promotionCost) || values.promotionCost <= 0 || !Number.isFinite(values.clickRate) || !Number.isFinite(values.actualRoi)) throw new Error(`请完整填写${label}的投放数据`);
if (values.clickRate !== Number((values.clicks / values.impressions * 100).toFixed(2))) throw new Error(`${label}的点击率应等于点击量 ÷ 曝光量 × 100%`);
if (values.actualRoi !== Number((values.turnover / values.promotionCost).toFixed(2))) throw new Error(`${label}的实际 ROI 应等于成交额 ÷ 推广花费`);
return values;
};
const seen = new Set();
channelRows.forEach((row) => { const channel = String(row?.channel || "").trim(); if (!channels.includes(channel) || seen.has(channel)) throw new Error("投放数据渠道必须与第一步完全一致"); seen.add(channel); const values = validateRow(row, `“${channel}”`); ["impressions", "clicks", "turnover", "promotionCost"].forEach((key) => { sum[key] += values[key]; }); });
if (seen.size !== channels.length) throw new Error("投放数据渠道必须与第一步完全一致");
const total = rows[rows.length - 1];
if (String(total?.channel || "").trim() !== "合计") throw new Error("合计必须固定填写在最后一行");
const totalValues = validateRow(total, "合计");
if (["impressions", "clicks", "turnover", "promotionCost"].some((key) => totalValues[key] !== Number(sum[key].toFixed(2)))) throw new Error("合计行的曝光量、点击量、成交额和推广花费必须等于所有渠道数据之和");
return Promise.resolve({ code: 200, data: { valid: true, message: "保本 ROI 与投放数据校验通过", breakEvenRoi } });
} catch (error) { return Promise.reject(error); }
}
return request({ url: "/api/student-training-answers/promotion-effect-optimization/step-3/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function validatePromotionEffectOptimizationStepFour(payload) {
const value = payload || {};
const rows = Array.isArray(value.rows) ? value.rows : [];
if (isStudentDemo()) {
try {
const decision = String(value.optimizationDecision || "").trim();
if (!decision) throw new Error("请填写整体优化决策");
const saved = demoResult("promotion-effect-optimization") || {};
const stepThree = typeof saved.step3Answer === "string" ? JSON.parse(saved.step3Answer || "{}") : saved.step3Answer || {};
const breakEvenRoi = Number(stepThree.breakEvenRoi);
const sourceRows = Array.isArray(stepThree.rows) ? stepThree.rows : [];
const channelRois = new Map(sourceRows.filter((row) => row?.channel !== "合计").map((row) => [String(row?.channel || "").trim(), Number(row?.actualRoi)]));
if (!Number.isFinite(breakEvenRoi) || !channelRois.size || rows.length !== channelRois.size) throw new Error("请先完成并保存第三步保本 ROI 与投放数据核算");
const seen = new Set();
rows.forEach((row) => {
const channel = String(row?.channel || "").trim();
const expectedRoi = channelRois.get(channel);
const actualRoi = Number(row?.roi);
const shouldAchieve = expectedRoi >= breakEvenRoi;
if (!Number.isFinite(expectedRoi) || seen.has(channel) || actualRoi !== expectedRoi) throw new Error("评估渠道与 ROI 必须和第三步保持一致");
if (row?.achievement !== (shouldAchieve ? "达标" : "不达标")) throw new Error(`“${channel}”的是否达标判断不正确`);
if (!shouldAchieve && (!String(row?.problemDiagnosis || "").trim() || !String(row?.optimizationAction || "").trim())) throw new Error(`“${channel}”未达标,必须填写问题诊断和优化动作`);
seen.add(channel);
});
const total = sourceRows.find((row) => row?.channel === "合计");
const overallRoi = Number(total?.actualRoi);
return Promise.resolve({ code: 200, data: { valid: true, message: "投放效果评估与优化决策校验通过", overallRoi, breakEvenRoi, overallAchievement: overallRoi >= breakEvenRoi ? "达标" : "不达标" } });
} catch (error) { return Promise.reject(error); }
}
return request({ url: "/api/student-training-answers/promotion-effect-optimization/step-4/validate", method: "post", data: { rows, optimizationDecision: value.optimizationDecision }, headers: { repeatSubmit: false } });
}
export function validatePurchaseDemandForecastStepOne(payload) {
const items = Array.isArray(payload?.items) ? payload.items : [];
if (isStudentDemo()) {
try {
const expectedTimes = [13, 14, 15];
if (items.length !== expectedTimes.length) throw new Error("请完整填写未来3个月的市场需求量");
const normalizedItems = expectedTimes.map((time, index) => {
const item = items.find((entry) => Number(entry?.time) === time);
const demand = Number(item?.demand);
if (!item || !Number.isInteger(demand) || demand < 0) {
throw new Error(`请填写预测第${index + 1}个月的非负整数需求量`);
}
return { time, demand };
});
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过", items: normalizedItems } });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/purchase-demand-forecast/step-1/validate",
method: "post",
data: { items },
headers: { repeatSubmit: false },
});
}
export function validateCategoryOptimizationStepOne(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
const tolerance = 0.01;
if (!rows.length) return Promise.reject(new Error("请至少填写一行品类数据"));
if (rows.length > 30) return Promise.reject(new Error("品类数据最多填写30行"));
const numbers = rows.map((row, index) => ({
index,
categoryName: String(row?.categoryName || "").trim(),
revenue: Number(row?.revenue),
revenueShare: Number(row?.revenueShare),
cumulativeShare: Number(row?.cumulativeShare),
}));
const incomplete = numbers.find((row) => !row.categoryName || [row.revenue, row.revenueShare, row.cumulativeShare].some((value) => !Number.isFinite(value) || value < 0));
if (incomplete) return Promise.reject(new Error(`第${incomplete.index + 1}行:请完整填写品类名称、近12个月营收、营收占比和累计占比`));
const totalRevenue = numbers.reduce((sum, row) => sum + row.revenue, 0);
if (totalRevenue <= 0) return Promise.reject(new Error("近12个月营收总和必须大于0"));
let accumulated = 0;
for (const row of numbers) {
const previous = numbers[row.index - 1];
if (previous && previous.revenue < row.revenue) return Promise.reject(new Error(`第${row.index + 1}行:近12个月营收需按从高到低排序`));
if (Math.abs(row.revenueShare - (row.revenue / totalRevenue) * 100) > tolerance) return Promise.reject(new Error(`第${row.index + 1}行:营收占比与近12个月营收不一致`));
accumulated += row.revenueShare;
if (Math.abs(row.cumulativeShare - accumulated) > tolerance) return Promise.reject(new Error(`第${row.index + 1}行:累计占比应等于从第一行到当前行的营收占比之和`));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/category-optimization/step-1/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } });
}
export function validateCategoryOptimizationStepTwo(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
if (!rows.length) return Promise.reject(new Error("请先完成第一步品类营收数据填写"));
for (const [index, row] of rows.entries()) {
const cumulativeShare = Number(row?.cumulativeShare);
const expected = cumulativeShare <= 70 ? "A" : cumulativeShare <= 90 ? "B" : "C";
if (!["A", "B", "C"].includes(row?.abcClassification)) return Promise.reject(new Error(`第${index + 1}行:请选择ABC分层判定`));
if (row.abcClassification !== expected) return Promise.reject(new Error(`第${index + 1}行:累计营收占比为${row.cumulativeShare}% ,应判定为${expected}类`));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "ABC分层校验通过" } });
}
return request({ url: "/api/student-training-answers/category-optimization/step-2/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } });
}
export function validateCategoryOptimizationStepThree(payload) {
const imageUrl = String(payload?.imageUrl || "").trim();
if (isStudentDemo()) {
if (!imageUrl) return Promise.reject(new Error("请上传帕累托图"));
return Promise.resolve({ code: 200, data: { valid: true, message: "帕累托图上传校验通过" } });
}
return request({ url: "/api/student-training-answers/category-optimization/step-3/validate", method: "post", data: { imageUrl }, headers: { repeatSubmit: false } });
}
export function validateCategoryOptimizationStepFour(payload) {
const value = payload || {};
const fields = ["productName", "matrixQuadrant", "matrixReason", "necessityAnalysis", "recommendedClassification", "trafficSupportMeasures"];
if (isStudentDemo()) {
if (fields.some((field) => !String(value[field] || "").trim())) return Promise.reject(new Error("请完整填写新品矩阵定位、调整必要性与流量扶持措施"));
const product = (Array.isArray(value.availableRows) ? value.availableRows : []).find((row) => row?.categoryName === value.productName);
if (!product) return Promise.reject(new Error("新品名称必须为第一步品类列表中的一项"));
if (value.recommendedClassification === product.abcClassification) return Promise.reject(new Error("建议调整分层不能与当前分层相同"));
return Promise.resolve({ code: 200, data: { valid: true, message: "新品分层优化校验通过", currentClassification: product.abcClassification } });
}
const data = Object.fromEntries(fields.map((field) => [field, value[field]]));
return request({ url: "/api/student-training-answers/category-optimization/step-4/validate", method: "post", data, headers: { repeatSubmit: false } });
}
export function validateCategoryOptimizationStepFive(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
const layers = new Set(rows.map((row) => row?.layer));
const complete = rows.length === 4 && ["A", "B", "C", "NEW"].every((layer) => layers.has(layer)) && rows.every((row) => String(row?.operationStrategy || "").trim() && String(row?.inventoryStrategy || "").trim() && String(row?.resourcePriority || "").trim());
if (!complete) return Promise.reject(new Error("请完整填写A、B、C类及新品四行的运营策略、库存策略和资源配置优先级"));
return Promise.resolve({ code: 200, data: { valid: true, message: "差异化运营策略校验通过" } });
}
return request({ url: "/api/student-training-answers/category-optimization/step-5/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } });
}
const requirementsDocumentStepOneAnswers = {
"document-info": "document-governance",
"product-overview": "product-boundary",
"user-needs-scenarios": "user-context",
"core-functional-requirements": "functional-scope",
"non-functional-requirements": "quality-attributes",
"hardware-spec-design": "hardware-constraints",
"testing-requirements": "acceptance-criteria",
"project-planning": "resource-plan",
"change-management": "change-control",
};
export function checkRequirementsDocumentStepOne(payload) {
const assignments = payload?.assignments && typeof payload.assignments === "object" ? payload.assignments : {};
if (isStudentDemo()) {
const expectedModules = Object.keys(requirementsDocumentStepOneAnswers);
const wrongModuleIds = expectedModules.filter((moduleId) => assignments[moduleId] !== requirementsDocumentStepOneAnswers[moduleId]);
const allCorrect = wrongModuleIds.length === 0 && Object.keys(assignments).length === expectedModules.length;
return Promise.resolve({
code: 200,
data: {
allCorrect,
correctCount: expectedModules.length - wrongModuleIds.length,
totalCount: expectedModules.length,
message: allCorrect ? "匹配正确,已解锁第 2 步。" : "当前匹配尚未全部正确,请继续调整。",
},
});
}
return request({
url: "/api/student-training-answers/requirements-doc/step-1/check",
method: "post",
data: { assignments },
headers: { repeatSubmit: false },
});
}
export function checkMarketOpportunitySelectionStepOne(items) {
const expected = {
"search-count": "市场规模", "demand-supply-ratio": "竞争强度", "transaction-amount": "市场规模", "transaction-growth": "市场潜力",
"category-transaction-growth": "市场潜力", "online-products": "竞争强度", "online-merchants": "竞争强度", "organic-traffic-ratio": "运营难度", "return-rate": "运营难度",
};
if (isStudentDemo()) {
const list = Array.isArray(items) ? items : [];
const incorrect = list.find((item) => expected[item?.id] !== item?.category);
if (list.length !== Object.keys(expected).length || incorrect) return Promise.reject(new Error("指标分类有误,请根据提示重新选择"));
return Promise.resolve({ code: 200, data: { correctCount: Object.keys(expected).length, totalCount: Object.keys(expected).length, allCorrect: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/market-opportunity-selection/step-1/check", method: "post", data: { items }, headers: { repeatSubmit: false } });
}
export function checkMarketOpportunitySelectionStepTwo(payload) {
const primaryWeights = Array.isArray(payload?.primaryWeights) ? payload.primaryWeights : [];
const dimensionWeights = Array.isArray(payload?.dimensionWeights) ? payload.dimensionWeights : [];
const groups = ["市场规模", "市场潜力", "竞争强度", "运营难度"];
const expectedIds = ["search-count", "demand-supply-ratio", "transaction-amount", "transaction-growth", "category-transaction-growth", "online-products", "online-merchants", "organic-traffic-ratio", "return-rate"];
if (isStudentDemo()) {
const primaryMap = Object.fromEntries(primaryWeights.map((item) => [item?.group, Number(item?.weight)]));
const dimensionMap = Object.fromEntries(dimensionWeights.map((item) => [item?.id, Number(item?.weight)]));
const groupsByIndicator = { "search-count": "市场规模", "transaction-amount": "市场规模", "transaction-growth": "市场潜力", "category-transaction-growth": "市场潜力", "demand-supply-ratio": "竞争强度", "online-products": "竞争强度", "online-merchants": "竞争强度", "organic-traffic-ratio": "运营难度", "return-rate": "运营难度" };
const isWeight = (value) => Number.isInteger(value) && value >= 1 && value <= 100;
const validPrimary = primaryWeights.length === groups.length && groups.every((group) => isWeight(primaryMap[group])) && groups.reduce((sum, group) => sum + primaryMap[group], 0) === 100;
const validDimensions = dimensionWeights.length === expectedIds.length && expectedIds.every((id) => isWeight(dimensionMap[id])) && groups.every((group) => expectedIds.filter((id) => groupsByIndicator[id] === group).reduce((sum, id) => sum + dimensionMap[id], 0) === 100);
if (!validPrimary || !validDimensions) {
return Promise.reject(new Error("一级指标权重合计和各类维度内权重合计均须为 100%"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/market-opportunity-selection/step-2/validate", method: "post", data: { primaryWeights, dimensionWeights }, headers: { repeatSubmit: false } });
}
export function checkMarketOpportunitySelectionStepThree(payload) {
const scores = Array.isArray(payload?.scores) ? payload.scores : [];
const expectedIds = ["smart-fitness-mirror", "smart-bathroom-mirror", "smart-beauty-mirror"];
if (isStudentDemo()) {
const ids = new Set(scores.map((item) => item?.id));
const isScore = (value) => Number.isFinite(Number(value)) && Number(value) >= 0 && Number(value) <= 100 && Math.round(Number(value) * 100) === Number(value) * 100;
if (scores.length !== expectedIds.length || expectedIds.some((id) => !ids.has(id)) || scores.some((item) => !isScore(item?.score))) {
return Promise.reject(new Error("请为三类产品填写 0 至 100 分、最多保留两位小数的综合得分"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/market-opportunity-selection/step-3/validate", method: "post", data: { scores }, headers: { repeatSubmit: false } });
}
export function checkMarketOpportunitySelectionStepFour(payload) {
const visualizationUrl = String(payload?.visualizationUrl || "").trim();
if (isStudentDemo()) {
if (!visualizationUrl) return Promise.reject(new Error("请上传数据可视化结果图"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/market-opportunity-selection/step-4/validate", method: "post", data: { visualizationUrl }, headers: { repeatSubmit: false } });
}
export function checkMarketOpportunitySelectionStepFive(payload) {
const insights = Array.isArray(payload?.insights) ? payload.insights : [];
const expectedIds = ["smart-fitness-mirror", "smart-bathroom-mirror", "smart-beauty-mirror"];
const isRate = (value) => Number.isFinite(Number(value)) && Number(value) >= 0 && Number(value) <= 100 && Math.round(Number(value) * 100) === Number(value) * 100;
const isFilled = (value) => String(value || "").trim().length > 0;
if (isStudentDemo()) {
const ids = new Set(insights.map((item) => item?.productId));
if (insights.length !== expectedIds.length || expectedIds.some((id) => !ids.has(id)) || insights.some((item) => !isRate(item?.favorableRate) || !isFilled(item?.painPoint) || !isFilled(item?.unmetNeed))) {
return Promise.reject(new Error("请完整填写三类产品的好评率、用户痛点和未被满足的需求;好评率为 0 至 100,最多保留两位小数"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/market-opportunity-selection/step-5/validate", method: "post", data: { insights }, headers: { repeatSubmit: false } });
}
export function checkMarketOpportunitySelectionStepSix(payload) {
const developmentConclusion = String(payload?.developmentConclusion || "").trim();
if (isStudentDemo()) {
if (!developmentConclusion) return Promise.reject(new Error("请填写建议开发的产品及具体原因"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/market-opportunity-selection/step-6/validate", method: "post", data: { developmentConclusion }, headers: { repeatSubmit: false } });
}
export function checkProductDevelopmentProcessStepOne(items) {
if (isStudentDemo()) {
const results = Array.isArray(items)
? items.map((item) => {
const correct = productDevelopmentProcessStepOneAnswers[item?.taskNo] === item?.classification;
return { taskNo: item?.taskNo, correct, message: correct ? "分类正确" : "分类有误,请重新选择" };
})
: [];
const correctCount = results.filter((item) => item.correct).length;
return Promise.resolve({
code: 200,
data: {
items: results,
correctCount,
totalCount: results.length,
allCorrect: results.length === Object.keys(productDevelopmentProcessStepOneAnswers).length && correctCount === results.length,
},
});
}
return request({
url: "/api/student-training-answers/product-development-process/step-1/check",
method: "post",
data: { items },
headers: { repeatSubmit: false },
});
}
export function checkProductArchitectureDesignStepOne(layerIds) {
const expected = ["hardware-physical", "driver-sensing", "algorithm-processing", "application-function"];
const items = Array.isArray(layerIds) ? layerIds : [];
if (isStudentDemo()) {
const correctPositionCount = expected.reduce((total, id, index) => total + (items[index] === id ? 1 : 0), 0);
return Promise.resolve({
code: 200,
data: {
correctPositionCount,
totalCount: expected.length,
allCorrect: items.length === expected.length && correctPositionCount === expected.length,
message: correctPositionCount === expected.length ? "排序正确,已解锁下一步。" : "排序尚未正确,请继续调整。",
},
});
}
return request({
url: "/api/student-training-answers/product-structure/step-1/check",
method: "post",
data: { layerIds: items },
headers: { repeatSubmit: false },
skipErrorMessage: true,
});
}
export function checkAfterSalesServiceStepOne(items) {
const expected = {
"voice-wake-no-response": "local-hardware-simple",
"voice-control-cannot-switch": "local-hardware-simple",
"app-cannot-find-device": "operation-network",
"network-setup-timeout": "operation-network",
"remote-control-intermittent": "cloud-connection",
"frequent-disconnection": "cloud-connection",
"indicator-no-response": "severe-fault",
"water-damage-no-response": "severe-fault",
};
const rows = Array.isArray(items) ? items : [];
if (isStudentDemo()) {
const correctCount = rows.filter((item) => expected[item?.faultId] === item?.categoryId).length;
return Promise.resolve({
code: 200,
data: {
correctCount,
totalCount: Object.keys(expected).length,
allCorrect: rows.length === Object.keys(expected).length && correctCount === Object.keys(expected).length,
message: correctCount === Object.keys(expected).length ? "分类正确,已解锁下一步。" : "分类尚未完全正确,请继续调整。",
},
});
}
return request({
url: "/api/student-training-answers/after-sales-service/step-1/check",
method: "post",
data: { items: rows },
headers: { repeatSubmit: false },
});
}
export function checkAfterSalesServiceStepTwo(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) {
const expectedIds = ["operation-network", "cloud-connection", "local-hardware-simple", "severe-fault", "out-of-warranty"];
const valid = rows.length === expectedIds.length
&& rows.every((row, index) => row?.id === expectedIds[index]
&& Boolean(String(row?.process || "").trim())
&& Boolean(String(row?.rule || "").trim()));
return Promise.resolve({
code: 200,
data: { valid, message: valid ? "服务分级与处理方案填写完整。" : "请完成五类故障的处理流程和核心规则。" },
});
}
return request({
url: "/api/student-training-answers/after-sales-service/step-2/validate",
method: "post",
data: { rows },
headers: { repeatSubmit: false },
});
}
export function checkAfterSalesServiceStepThree(payload) {
const policies = Array.isArray(payload?.policies) ? payload.policies : [];
const summaries = Array.isArray(payload?.summaries) ? payload.summaries : [];
const policyIds = ["return-policy", "warranty-policy", "repair-service", "response-sla", "compensation-plan"];
const summaryIds = ["warranty-period", "response-commitment", "featured-service"];
if (isStudentDemo()) {
const valid = policies.length === policyIds.length
&& policies.every((row, index) => row?.id === policyIds[index]
&& Boolean(String(row?.content || "").trim())
&& Boolean(String(row?.condition || "").trim()))
&& summaries.length === summaryIds.length
&& summaries.every((row, index) => row?.id === summaryIds[index] && Boolean(String(row?.content || "").trim()));
return Promise.resolve({
code: 200,
data: { valid, message: valid ? "售后政策框架与详情页摘要填写完整。" : "请完成五类售后政策及三项详情页售后信息。" },
});
}
return request({
url: "/api/student-training-answers/after-sales-service/step-3/validate",
method: "post",
data: { policies, summaries },
headers: { repeatSubmit: false },
});
}
export function checkProductArchitectureDesignStepTwo(actionIds) {
const expected = ["user-action", "hardware-senses", "driver-transmits", "algorithm-processes", "application-triggers", "driver-outputs", "hardware-effects"];
const items = Array.isArray(actionIds) ? actionIds : [];
if (isStudentDemo()) {
const correctPositionCount = expected.reduce((total, id, index) => total + (items[index] === id ? 1 : 0), 0);
return Promise.resolve({
code: 200,
data: {
correctPositionCount,
totalCount: expected.length,
allCorrect: items.length === expected.length && correctPositionCount === expected.length,
message: correctPositionCount === expected.length ? "排序正确,已解锁下一步。" : "排序尚未正确,请继续调整。",
},
});
}
return request({
url: "/api/student-training-answers/product-structure/step-2/check",
method: "post",
data: { actionIds: items },
headers: { repeatSubmit: false },
skipErrorMessage: true,
});
}
export function checkProductArchitectureDesignStepThree(rows) {
const items = Array.isArray(rows) ? rows : [];
const requiredFields = ["userScenario", "hardwareSense", "driverTransmit", "algorithmProcess", "applicationTrigger", "driverOutput", "hardwareEffect"];
if (isStudentDemo()) {
const valid = items.length >= 3 && items.length <= 10 && items.every((row) => requiredFields.every((field) => Boolean(String(row?.[field] || "").trim())));
return Promise.resolve({
code: 200,
data: { valid, message: valid ? "场景架构分析填写完整。" : "请至少完整填写 3 个典型工作场景。" },
});
}
return request({
url: "/api/student-training-answers/product-structure/step-3/validate",
method: "post",
data: { rows: items },
headers: { repeatSubmit: false },
skipErrorMessage: true,
});
}
export function checkProductArchitectureDesignStepFour(images) {
const files = Array.isArray(images) ? images : [];
if (isStudentDemo()) {
const valid = files.length === 3 && files.every((item) => typeof item === "string" && item.trim());
return Promise.resolve({
code: 200,
data: { valid, message: valid ? "三张核心功能架构图已提交。" : "请上传三张核心功能架构图。" },
});
}
return request({
url: "/api/student-training-answers/product-structure/step-4/validate",
method: "post",
data: { images: files },
headers: { repeatSubmit: false },
skipErrorMessage: true,
});
}
export function checkProductDevelopmentProcessStepTwo(items) {
if (isStudentDemo()) {
const rows = Array.isArray(items) ? items : [];
const incomplete = rows.length !== Object.keys(productDevelopmentProcessStepOneAnswers).length || rows.find((row) => {
const startDate = String(row?.startDate || "").trim();
const endDate = String(row?.endDate || "").trim();
return !startDate || !endDate || !String(row?.dependency || "").trim() || startDate > endDate
|| productDevelopmentProcessStepOneAnswers[row?.taskNo] !== row?.classification;
});
if (incomplete) return Promise.reject(new Error("请完整填写任务工期和项目依赖关系,并先完成第一步关键任务判断"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/product-development-process/step-2/validate",
method: "post",
data: { items },
headers: { repeatSubmit: false },
skipErrorMessage: true,
});
}
export function checkProductDevelopmentProcessStepThree(payload) {
if (isStudentDemo()) {
const value = payload || {};
if (!String(value.ganttImageUrl || "").trim() || !/\.(png|jpe?g|webp|gif)$/i.test(String(value.ganttImageName || ""))) {
return Promise.reject(new Error("请上传甘特图图片(PNG、JPG、JPEG、WEBP 或 GIF)"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/product-development-process/step-3/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
export function checkProductDevelopmentProcessStepFour(payload) {
if (isStudentDemo()) {
const value = payload || {};
if (!String(value.parallelWorkAnalysis || "").trim()) return Promise.reject(new Error("请填写可提前介入并同步完成的电商运营工作"));
if (!String(value.aiScenarioOne || "").trim() || !String(value.aiScenarioTwo || "").trim()) {
return Promise.reject(new Error("请至少填写 2 个 AI 工具提效应用场景"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/product-development-process/step-4/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
function validateNewProductSurveyStepTwoDemo(form) {
const value = form || {};
const images = Array.isArray(value.images) ? value.images : [];
if (!['京东', '天猫', '抖音电商'].includes(String(value.platform || '').trim())) {
throw new Error('请选择调查平台');
}
if (!String(value.scene || '').trim()) {
throw new Error('请选择调查场景');
}
if (value.scene === '其他场景' && !String(value.customScene || '').trim()) {
throw new Error('请填写自定义调查场景');
}
if (!String(value.keyword || '').trim()) {
throw new Error('请填写搜索关键词');
}
if (!String(value.productName || '').trim()) {
throw new Error('请填写产品名称');
}
if (images.length < 1 || images.length > 5 || images.some((image) => !String(image?.url || '').trim())) {
throw new Error('请上传 1 至 5 张产品图片');
}
return { valid: true, message: '校验通过' };
}
export function checkNewProductSurveyStepTwo(form) {
if (isStudentDemo()) {
try {
return Promise.resolve({ code: 200, data: validateNewProductSurveyStepTwoDemo(form) });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: '/api/student-training-answers/new-product-survey/step-2/validate',
method: 'post',
data: { ...form, imageUrls: (form?.images || []).map((image) => image?.url) },
headers: { repeatSubmit: false },
});
}
function validateNewProductSurveyStepThreeDemo(payload) {
const expectedDimensions = ["核心层", "有形层", "延伸层"];
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (rows.length !== expectedDimensions.length) {
throw new Error("请完成核心层、有形层、延伸层分析");
}
const dimensions = rows.map((row) => String(row?.dimension || "").trim());
if (new Set(dimensions).size !== expectedDimensions.length || expectedDimensions.some((dimension) => !dimensions.includes(dimension))) {
throw new Error("分析维度不完整或重复");
}
const invalidRow = rows.find((row) => {
const points = Array.isArray(row?.points) ? row.points : [];
return points.map((point) => String(point || "").trim()).filter(Boolean).length < 2;
});
if (invalidRow) {
throw new Error(`${invalidRow.dimension || "该层"}至少填写 2 条分析要点`);
}
return { valid: true, message: "校验通过" };
}
export function checkNewProductSurveyStepThree(payload) {
if (isStudentDemo()) {
try {
return Promise.resolve({ code: 200, data: validateNewProductSurveyStepThreeDemo(payload) });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/new-product-survey/step-3/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
function validateNewProductSurveyStepFourDemo(form) {
const value = form || {};
const fields = [
["coreValue", "核心层"],
["tangibleValue", "有形层"],
["extendedValue", "延伸层"],
];
for (const [field, label] of fields) {
if (!String(value[field] || "").trim()) {
throw new Error(`请填写${label}AI应用价值分析`);
}
}
return { valid: true, message: "校验通过" };
}
export function checkNewProductSurveyStepFour(form) {
if (isStudentDemo()) {
try {
return Promise.resolve({ code: 200, data: validateNewProductSurveyStepFourDemo(form) });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/new-product-survey/step-4/validate",
method: "post",
data: form,
headers: { repeatSubmit: false },
});
}
const productDevelopmentFactorsStepOneDimensions = ["用户需求真实性", "产品体验闭环", "生态与兼容"];
function validateProductDevelopmentFactorsStepOneDemo(payload) {
const factors = Array.isArray(payload?.factors) ? payload.factors : [];
if (factors.length !== productDevelopmentFactorsStepOneDimensions.length) {
throw new Error("请完成用户需求真实性、产品体验闭环和生态与兼容分析");
}
const dimensions = factors.map((factor) => String(factor?.dimension || "").trim());
if (new Set(dimensions).size !== productDevelopmentFactorsStepOneDimensions.length
|| productDevelopmentFactorsStepOneDimensions.some((dimension) => !dimensions.includes(dimension))) {
throw new Error("提交的分析维度不完整或重复");
}
const incompleteFactor = factors.find((factor) => !String(factor?.successAnalysis || "").trim()
|| !String(factor?.failedAnalysis || "").trim());
if (incompleteFactor) {
throw new Error(`请完成${incompleteFactor.dimension || "该项"}的成功产品和失败产品分析`);
}
return { valid: true, message: "校验通过" };
}
export function checkProductDevelopmentFactorsStepOne(payload) {
if (isStudentDemo()) {
try {
return Promise.resolve({ code: 200, data: validateProductDevelopmentFactorsStepOneDemo(payload) });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/product-development-factors/step-1/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
const productDevelopmentFactorsStepTwoDimensions = ["技术应用", "合规与认证"];
function validateProductDevelopmentFactorsStepTwoDemo(payload) {
const factors = Array.isArray(payload?.factors) ? payload.factors : [];
const dimensions = factors.map((factor) => String(factor?.dimension || "").trim());
if (factors.length !== 2 || new Set(dimensions).size !== 2 || productDevelopmentFactorsStepTwoDimensions.some((dimension) => !dimensions.includes(dimension))) throw new Error("请完成技术应用和合规与认证分析");
const incomplete = factors.find((factor) => !String(factor?.successAnalysis || "").trim() || !String(factor?.failedAnalysis || "").trim());
if (incomplete) throw new Error(`请完成${incomplete.dimension || "该项"}的成功产品和失败产品分析`);
return { valid: true, message: "校验通过" };
}
export function checkProductDevelopmentFactorsStepTwo(payload) {
if (isStudentDemo()) { try { return Promise.resolve({ code: 200, data: validateProductDevelopmentFactorsStepTwoDemo(payload) }); } catch (error) { return Promise.reject(error); } }
return request({ url: "/api/student-training-answers/product-development-factors/step-2/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}
const productDevelopmentFactorsStepThreeDimensions = ["供应链与成本控制", "商业模式", "市场反馈"];
function validateProductDevelopmentFactorsStepThreeDemo(payload) {
const factors = Array.isArray(payload?.factors) ? payload.factors : [];
const dimensions = factors.map((factor) => String(factor?.dimension || "").trim());
if (factors.length !== productDevelopmentFactorsStepThreeDimensions.length
|| new Set(dimensions).size !== productDevelopmentFactorsStepThreeDimensions.length
|| productDevelopmentFactorsStepThreeDimensions.some((dimension) => !dimensions.includes(dimension))) {
throw new Error("请完成供应链与成本控制、商业模式和市场反馈分析");
}
const incomplete = factors.find((factor) => !String(factor?.successAnalysis || "").trim()
|| !String(factor?.failedAnalysis || "").trim());
if (incomplete) throw new Error(`请完成${incomplete.dimension || "该项"}的成功产品和失败产品分析`);
return { valid: true, message: "校验通过" };
}
export function checkProductDevelopmentFactorsStepThree(payload) {
if (isStudentDemo()) {
try { return Promise.resolve({ code: 200, data: validateProductDevelopmentFactorsStepThreeDemo(payload) }); }
catch (error) { return Promise.reject(error); }
}
return request({ url: "/api/student-training-answers/product-development-factors/step-3/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}
export function checkProductDevelopmentFactorsStepFour(payload) {
const value = payload || {};
if (isStudentDemo()) {
if (!String(value.successFactors || "").trim()) return Promise.reject(new Error("请填写产品成功的关键因素"));
if (!String(value.failedFactors || "").trim()) return Promise.reject(new Error("请填写产品失败的关键因素"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/product-development-factors/step-4/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}
export function checkConsumerBehaviorStepOne(payload) {
const hypotheses = Array.isArray(payload?.hypotheses) ? payload.hypotheses : [];
if (isStudentDemo()) {
const incomplete = hypotheses.find((item) => ["targetGroup", "keyFinding", "coreHypothesis"].some((field) => !String(item?.[field] || "").trim()));
if (!hypotheses.length || incomplete) {
return Promise.reject(new Error("请完整填写目标人群、实训8关键发现和问卷核心假设"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/consumer-behavior/step-1/validate",
method: "post",
data: { hypotheses },
headers: { repeatSubmit: false },
});
}
export function checkConsumerBehaviorStepTwo(payload) {
const needs = Array.isArray(payload?.needs) ? payload.needs : [];
const allowedTypes = ["基本型", "期望型", "兴奋型", "无差异型", "反向型"];
if (isStudentDemo()) {
const incomplete = needs.find((item) => !String(item?.feature || "").trim()
|| !allowedTypes.includes(item?.kanoType) || !String(item?.featureDirection || "").trim());
if (!needs.length || needs.length > 20 || incomplete) {
return Promise.reject(new Error("请完整填写功能、KANO类型和对应功能方向(最多20行)"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/consumer-behavior/step-2/validate",
method: "post",
data: { needs },
headers: { repeatSubmit: false },
});
}
export function checkConsumerBehaviorStepThree(payload) {
if (isStudentDemo()) {
const importedDocumentUrl = String(payload?.importedDocumentUrl || "").trim();
const questions = Array.isArray(payload?.questions) ? payload.questions : [];
const requiredSections = ["基本信息", "购买行为", "需求偏好", "开放建议"];
const validQuestions = questions.filter((item) => String(item?.title || "").trim());
const complete = requiredSections.every((section) => validQuestions.some((item) => item.section === section))
&& validQuestions.some((item) => item.section === "需求偏好" && item.type === "LIKERT");
if (!importedDocumentUrl && !complete) {
return Promise.reject(new Error("请完整设计包含四个部分且含Likert五级量表的问卷,或导入Word问卷"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/consumer-behavior/step-3/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
export function previewConsumerBehaviorQuestionnaireWord(data) {
if (isStudentDemo()) {
return Promise.reject(new Error("演示模式不支持 Word 文档预览"));
}
return request({
url: "/api/student-training-answers/consumer-behavior/step-3/word-preview",
method: "post",
data,
headers: {
"Content-Type": "multipart/form-data",
repeatSubmit: false,
},
});
}
export function previewRequirementsDocumentFile(data) {
if (isStudentDemo()) {
return Promise.resolve({
code: 200,
data: {
fileType: "DEMO",
content: "演示模式下仅展示已选择文件的信息;登录学生账号后可解析并在线预览 Word 或 PDF 的正文内容。",
},
});
}
return request({
url: "/api/student-training-answers/requirements-doc/step-3/document-preview",
method: "post",
data,
headers: {
"Content-Type": "multipart/form-data",
repeatSubmit: false,
},
});
}
export function checkConsumerBehaviorStepFour(payload) {
if (isStudentDemo()) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (rows.length < 5 || rows.some((item) => !String(item?.title || "").trim() || !String(item?.content || "").trim())) {
return Promise.reject(new Error("请完整填写 5 个问卷调研报告模块的标题和内容"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/consumer-behavior/step-4/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
export function checkProductValuePositioningStepOne(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
const types = ["核心痛点1", "核心痛点2", "核心痛点3", "弱需求", "伪需求"];
if (isStudentDemo()) {
const valid = rows.length === types.length && rows.every((row, index) => row?.id === index + 1
&& row?.type === types[index]
&& Boolean(String(row?.description || "").trim())
&& Boolean(String(row?.scene || "").trim()));
return Promise.resolve({
code: 200,
data: {
valid,
message: valid ? "核心痛点分类填写完整。" : "请完成 3 个核心痛点、弱需求和伪需求的描述与场景。",
},
});
}
return request({
url: "/api/student-training-answers/product-value-positioning/step-1/validate",
method: "post",
data: { rows },
headers: { repeatSubmit: false },
});
}
export function checkProductValuePositioningStepTwo(payload) {
const customerProfile = payload?.customerProfile || {};
const valueMap = payload?.valueMap || {};
if (isStudentDemo()) {
const requiredValues = [
customerProfile.jobs, customerProfile.pains, customerProfile.gains,
valueMap.products, valueMap.painRelievers, valueMap.gainCreators,
payload?.canvasUrl,
];
const valid = requiredValues.every((item) => Boolean(String(item || "").trim()));
return Promise.resolve({
code: 200,
data: {
valid,
message: valid ? "价值主张画布填写完整。" : "请完整填写用户档案、产品价值地图并上传产品价值主张画布。",
},
});
}
return request({
url: "/api/student-training-answers/product-value-positioning/step-2/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
export function checkProductValuePositioningStepThree(payload) {
const statements = Array.isArray(payload?.statements) ? payload.statements : [];
const requiredFields = ["targetUser", "productName", "productCategory", "coreValue", "differentiationReason"];
if (isStudentDemo()) {
const valid = statements.length === 2 && statements.every((statement, index) => statement?.id === index + 1
&& requiredFields.every((field) => Boolean(String(statement?.[field] || "").trim())));
return Promise.resolve({
code: 200,
data: { valid, message: valid ? "两版定位陈述填写完整。" : "请完整填写两个产品价值定位陈述版本。" },
});
}
return request({
url: "/api/student-training-answers/product-value-positioning/step-3/validate",
method: "post",
data: { statements },
headers: { repeatSubmit: false },
});
}
export function checkProductValuePositioningStepFour(payload) {
const sectionKeys = ["marketInsights", "marketOverview", "userAnalysis", "competitorAnalysis", "productRequirements"];
if (isStudentDemo()) {
const valid = sectionKeys.every((key) => Array.isArray(payload?.[key]) && payload[key].length > 0
&& payload[key].every((row) => Boolean(String(row?.content || "").trim())))
&& Boolean(String(payload?.productPositioning || "").trim());
return Promise.resolve({
code: 200,
data: { valid, message: valid ? "MRD 文档填写完整。" : "请完整填写 MRD 的五组分析内容和产品定位。" },
});
}
return request({
url: "/api/student-training-answers/product-value-positioning/step-4/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
export function validateHardwareCostStepOne(payload) {
const items = Array.isArray(payload?.items) ? payload.items : [];
if (isStudentDemo()) {
const normalizedItems = items.map((item) => {
const unitPrice = Number(item?.unitPrice);
const quantity = Number(item?.quantity);
return {
materialName: String(item?.materialName || "").trim(),
specification: String(item?.specification || "").trim(),
unitPrice,
quantity,
amount: Number.isFinite(unitPrice) && Number.isFinite(quantity)
? Number((unitPrice * quantity).toFixed(2))
: 0,
};
});
const valid = normalizedItems.length >= 10
&& normalizedItems.length <= 40
&& normalizedItems.every((item) => item.materialName && item.specification
&& Number.isFinite(item.unitPrice) && item.unitPrice >= 0
&& Number.isInteger(item.quantity) && item.quantity > 0);
if (!valid) {
return Promise.reject(new Error("请完整填写 10 至 40 行 BOM 物料明细,单价不能为负且数量必须大于 0"));
}
return Promise.resolve({
code: 200,
data: {
valid: true,
message: "校验通过",
items: normalizedItems,
totalAmount: Number(normalizedItems.reduce((total, item) => total + item.amount, 0).toFixed(2)),
},
});
}
return request({
url: "/api/student-training-answers/hardware-cost/step-1/validate",
method: "post",
data: { items },
headers: { repeatSubmit: false },
});
}
export function validateHardwareCostStepTwo(payload) {
const value = payload || {};
const items = Array.isArray(value.items) ? value.items : [];
if (isStudentDemo()) {
const smallBatchConstraint = Number(value.smallBatchConstraint);
const largeBatchConstraint = Number(value.largeBatchConstraint);
const normalizedItems = items.map((item) => ({
materialName: String(item?.materialName || "").trim(),
smallBatchUnitCost: Number(item?.smallBatchUnitCost),
largeBatchUnitCost: Number(item?.largeBatchUnitCost),
}));
const validRows = normalizedItems.length > 0 && normalizedItems.every((item) => item.materialName
&& Number.isFinite(item.smallBatchUnitCost) && item.smallBatchUnitCost >= 0
&& Number.isFinite(item.largeBatchUnitCost) && item.largeBatchUnitCost >= 0);
if (!validRows || !Number.isFinite(smallBatchConstraint) || smallBatchConstraint < 0
|| !Number.isFinite(largeBatchConstraint) || largeBatchConstraint < 0) {
return Promise.reject(new Error("请完整填写两种批量的物料成本和成本约束"));
}
const smallBatchTotal = Number(normalizedItems.reduce((sum, item) => sum + item.smallBatchUnitCost, 0).toFixed(2));
const largeBatchTotal = Number(normalizedItems.reduce((sum, item) => sum + item.largeBatchUnitCost, 0).toFixed(2));
const smallBatchWithinConstraint = smallBatchTotal <= smallBatchConstraint;
const largeBatchWithinConstraint = largeBatchTotal <= largeBatchConstraint;
return Promise.resolve({
code: 200,
data: {
valid: smallBatchWithinConstraint && largeBatchWithinConstraint,
message: smallBatchWithinConstraint && largeBatchWithinConstraint
? "两种批量的 BOM 成本均满足约束"
: "合计成本超出填写的成本约束",
items: normalizedItems,
smallBatchConstraint: Number(smallBatchConstraint.toFixed(2)),
largeBatchConstraint: Number(largeBatchConstraint.toFixed(2)),
smallBatchTotal,
largeBatchTotal,
smallBatchWithinConstraint,
largeBatchWithinConstraint,
},
});
}
return request({
url: "/api/student-training-answers/hardware-cost/step-2/validate",
method: "post",
data: value,
headers: { repeatSubmit: false },
});
}
export function validateHardwareCostStepThree(payload) {
const value = payload || {};
const items = Array.isArray(value.items) ? value.items : [];
if (isStudentDemo()) {
if (items.length < 6 || items.length > 15) {
return Promise.reject(new Error("请填写 6 至 15 项制造费用。"));
}
const normalizedItems = items.map((item, index) => {
const expenseName = String(item?.expenseName || "").trim();
const smallBatchUnitCost = Number(item?.smallBatchUnitCost);
const largeBatchUnitCost = Number(item?.largeBatchUnitCost);
if (!expenseName || !Number.isFinite(smallBatchUnitCost) || !Number.isFinite(largeBatchUnitCost)
|| smallBatchUnitCost < 0 || largeBatchUnitCost < 0) {
throw new Error("请完整填写第 " + (index + 1) + " 项制造费用及两种批量成本。");
}
return {
expenseName,
smallBatchUnitCost: Number(smallBatchUnitCost.toFixed(2)),
largeBatchUnitCost: Number(largeBatchUnitCost.toFixed(2)),
};
});
const saved = demoResult("hardware-cost");
const stepTwo = typeof saved?.step2Answer === "string" ? JSON.parse(saved.step2Answer) : saved?.step2Answer;
const smallBatchBomCost = Number(stepTwo?.smallBatchTotal);
const largeBatchBomCost = Number(stepTwo?.largeBatchTotal);
if (!Number.isFinite(smallBatchBomCost) || !Number.isFinite(largeBatchBomCost)) {
return Promise.reject(new Error("请先完成并保存第 2 步物料 BOM 成本核算。"));
}
const smallBatchManufacturingCost = Number(normalizedItems.reduce((sum, item) => sum + item.smallBatchUnitCost, 0).toFixed(2));
const largeBatchManufacturingCost = Number(normalizedItems.reduce((sum, item) => sum + item.largeBatchUnitCost, 0).toFixed(2));
return Promise.resolve({
code: 200,
data: {
valid: true,
message: "制造成本与单件总成本已核算完成。",
items: normalizedItems,
smallBatchManufacturingCost,
largeBatchManufacturingCost,
smallBatchBomCost: Number(smallBatchBomCost.toFixed(2)),
largeBatchBomCost: Number(largeBatchBomCost.toFixed(2)),
smallBatchUnitTotalCost: Number((smallBatchManufacturingCost + smallBatchBomCost).toFixed(2)),
largeBatchUnitTotalCost: Number((largeBatchManufacturingCost + largeBatchBomCost).toFixed(2)),
},
});
}
return request({
url: "/api/student-training-answers/hardware-cost/step-3/validate",
method: "post",
data: value,
headers: { repeatSubmit: false },
skipErrorMessage: true,
});
}
export function validateHardwareCostStepFour(payload) {
const value = payload || {};
if (isStudentDemo()) {
const smallBatchChartUrl = String(value.smallBatchChartUrl || "").trim();
const largeBatchChartUrl = String(value.largeBatchChartUrl || "").trim();
const optimizationPlan = String(value.optimizationPlan || "").trim();
if (!smallBatchChartUrl || !largeBatchChartUrl) {
return Promise.reject(new Error("请分别上传小批量试产和大批量量产成本结构图"));
}
if (!optimizationPlan) return Promise.reject(new Error("请填写成本优化决策"));
if (optimizationPlan.length > 3000) return Promise.reject(new Error("成本优化决策不能超过 3000 字"));
return Promise.resolve({ code: 200, data: { valid: true, message: "成本结构图与优化决策校验通过" } });
}
return request({
url: "/api/student-training-answers/hardware-cost/step-4/validate",
method: "post",
data: value,
headers: { repeatSubmit: false },
});
}
export function validateSupplierEvaluationStepOne(payload) {
const items = Array.isArray(payload?.items) ? payload.items : [];
if (isStudentDemo()) {
const expectedDimensions = ["采购成本", "交货周期", "品质可靠性", "采购灵活性", "服务支持"];
const normalizedItems = items.map((item) => ({
dimension: String(item?.dimension || "").trim(),
smallBatchWeight: Number(item?.smallBatchWeight),
largeBatchWeight: Number(item?.largeBatchWeight),
}));
const hasAllDimensions = normalizedItems.length === expectedDimensions.length
&& normalizedItems.every((item, index) => item.dimension === expectedDimensions[index]);
const validNumbers = normalizedItems.every((item) => Number.isFinite(item.smallBatchWeight)
&& Number.isFinite(item.largeBatchWeight)
&& item.smallBatchWeight >= 0 && item.smallBatchWeight <= 100
&& item.largeBatchWeight >= 0 && item.largeBatchWeight <= 100);
const smallBatchTotal = Number(normalizedItems.reduce((sum, item) => sum + item.smallBatchWeight, 0).toFixed(2));
const largeBatchTotal = Number(normalizedItems.reduce((sum, item) => sum + item.largeBatchWeight, 0).toFixed(2));
if (!hasAllDimensions || !validNumbers || smallBatchTotal !== 100 || largeBatchTotal !== 100) {
return Promise.reject(new Error("请完整填写 5 个评估维度,且小批量试产与大批量量产的权重合计均为 100%"));
}
return Promise.resolve({
code: 200,
data: { valid: true, message: "双场景权重校验通过", items: normalizedItems, smallBatchTotal, largeBatchTotal },
});
}
return request({
url: "/api/student-training-answers/supplier-evaluation/step-1/validate",
method: "post",
data: { items },
headers: { repeatSubmit: false },
});
}
export function validateSupplierEvaluationStepTwo(payload) {
const value = payload || {};
const expectedDimensions = ["采购成本", "交货周期", "品质可靠性", "采购灵活性", "服务支持"];
if (isStudentDemo()) {
try {
const saved = demoResult("supplier-evaluation");
const stepOne = typeof saved?.step1Answer === "string" ? JSON.parse(saved.step1Answer) : saved?.step1Answer;
const weights = Array.isArray(stepOne?.items) ? stepOne.items : [];
if (weights.length !== expectedDimensions.length
|| weights.some((item, index) => item?.dimension !== expectedDimensions[index])) {
throw new Error("请先完成并保存第 1 步的供应渠道权重设置");
}
const normalize = (rows, isSmallBatch, expectedChannels) => {
if (!Array.isArray(rows) || rows.length !== 5) throw new Error("试产和量产场景均需填写 5 个供应渠道");
return rows.map((row, rowIndex) => {
const channel = String(row?.channel || "").trim();
if (!channel) throw new Error(`请填写第 ${rowIndex + 1} 行供应渠道名称`);
if (expectedChannels && channel !== expectedChannels[rowIndex]) {
throw new Error("量产场景的供应渠道必须与试产场景保持一致");
}
const scores = Array.isArray(row?.scores) ? row.scores : [];
if (scores.length !== expectedDimensions.length) throw new Error(`请完整填写“${channel}”的 5 个维度评分`);
const normalizedScores = scores.map((score, index) => {
const numeric = Number(score?.score);
if (score?.dimension !== expectedDimensions[index] || !Number.isInteger(numeric) || numeric < 0 || numeric > 10) {
throw new Error(`请为“${channel}”填写 0 到 10 的整数评分:${expectedDimensions[index]}`);
}
return { dimension: expectedDimensions[index], score: numeric };
});
const weightedTotal = Number(normalizedScores.reduce((sum, score, index) => {
const weight = Number(isSmallBatch ? weights[index]?.smallBatchWeight : weights[index]?.largeBatchWeight);
return sum + score.score * weight / 100;
}, 0).toFixed(2));
return { channel, scores: normalizedScores, weightedTotal };
});
};
const smallBatchChannels = normalize(value.smallBatchChannels, true);
const largeBatchChannels = normalize(value.largeBatchChannels, false, smallBatchChannels.map((item) => item.channel));
return Promise.resolve({ code: 200, data: { valid: true, message: "两种采购场景的供应渠道加权得分已重新核算。", weights, smallBatchChannels, largeBatchChannels } });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/supplier-evaluation/step-2/validate",
method: "post",
data: value,
headers: { repeatSubmit: false },
});
}
export function validateSupplierEvaluationStepThree(payload) {
if (isStudentDemo()) {
try {
const value = payload || {};
const saved = demoResult("supplier-evaluation");
const stepTwo = typeof saved?.step2Answer === "string"
? JSON.parse(saved.step2Answer || "{}")
: (saved?.step2Answer || {});
const scenarioFields = {
smallBatch: "smallBatchChannels",
largeBatch: "largeBatchChannels",
};
const suppliedAnalyses = Array.isArray(value.scenarioAnalyses) ? value.scenarioAnalyses : [];
const scenarioAnalyses = Object.keys(scenarioFields).map((scenario) => {
const analysis = suppliedAnalyses.find((item) => item?.scenario === scenario);
if (!analysis || !String(analysis.differenceAnalysis || "").trim()) {
throw new Error("请填写两种采购场景的分差与分析");
}
const ranked = (Array.isArray(stepTwo[scenarioFields[scenario]]) ? stepTwo[scenarioFields[scenario]] : [])
.map((item) => ({
channel: String(item?.channel || "").trim(),
score: Number(item?.weightedTotal),
}))
.filter((item) => item.channel && Number.isFinite(item.score))
.sort((left, right) => right.score - left.score || left.channel.localeCompare(right.channel, "zh-CN"));
if (ranked.length < 2) {
throw new Error("请先完成第二步供应渠道评分");
}
if (analysis.bestChannel !== ranked[0].channel || analysis.secondChannel !== ranked[1].channel) {
throw new Error("渠道排名已更新,请确认第二步评分结果后再保存");
}
return {
scenario,
bestChannel: ranked[0].channel,
secondChannel: ranked[1].channel,
bestScore: ranked[0].score.toFixed(2),
secondScore: ranked[1].score.toFixed(2),
scoreGap: (ranked[0].score - ranked[1].score).toFixed(2),
differenceAnalysis: String(analysis.differenceAnalysis).trim(),
};
});
const procurementRecommendation = String(value.procurementRecommendation || "").trim();
if (!procurementRecommendation) {
throw new Error("请填写综合采购建议");
}
const risks = Array.isArray(value.risks) ? value.risks : [];
if (risks.length < 4 || risks.length > 10) {
throw new Error("供应风险需保留至少 4 条,最多 10 条");
}
const normalizedRisks = risks.map((risk) => {
const normalized = {
dimension: String(risk?.dimension || "").trim(),
description: String(risk?.description || "").trim(),
level: String(risk?.level || "").trim(),
measure: String(risk?.measure || "").trim(),
};
if (!normalized.dimension || !normalized.description || !normalized.measure || !["高", "中", "低"].includes(normalized.level)) {
throw new Error("请完整填写每条供应风险及应对措施");
}
return normalized;
});
return Promise.resolve({
code: 200,
data: {
valid: true,
message: "校验通过",
scenarioAnalyses,
procurementRecommendation,
risks: normalizedRisks,
},
});
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/supplier-evaluation/step-3/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
function validateBusinessFeasibilityStepOneDemo(payload) {
const value = payload || {}; const overview = value.projectOverview || {};
const requiredOverview = ["projectBackground", "productPositioning", "projectGoals", "macroEnvironmentConclusion", "competitiveLandscapeConclusion"];
if (requiredOverview.some((key) => !String(overview[key] || "").trim())) throw new Error("请完整填写项目概述");
const market = value.marketAnalysis || {}; const numericFields = ["annualMarketSize", "monthlyGrowthRate", "topBrandMarketShare", "marketConcentrationCr3"];
if (numericFields.some((key) => !Number.isFinite(Number(market[key])) || Number(market[key]) < 0) || !String(market.competitorsPriceRange || "").trim()) throw new Error("请完整填写市场规模与竞争格局分析");
const users = Array.isArray(value.targetUsers) ? value.targetUsers : [];
if (users.length !== 3 || users.some((user) => !String(user?.userGroup || "").trim() || !String(user?.scenario || "").trim() || !String(user?.coreNeed || "").trim() || !String(user?.spendingPower || "").trim())) throw new Error("请完整填写 3 类目标用户群体分析");
const models = Array.isArray(value.businessModels) ? value.businessModels : [];
if (models.length !== 2 || models.some((model) => !String(model?.module || "").trim() || !String(model?.strategy || "").trim() || !String(model?.explanation || "").trim())) throw new Error("请完整填写商业模式梳理");
const costIds = ["hardware-bom", "manufacturing", "rd-amortization", "channel-marketing", "logistics-warehouse"]; const costs = Array.isArray(value.costs) ? value.costs : [];
if (costs.length !== costIds.length || costIds.some((id) => !costs.some((cost) => cost?.id === id && Number.isFinite(Number(cost?.amount)) && Number(cost.amount) >= 0))) throw new Error("请填写全部成本项目");
const totalCost = costs.reduce((total, cost) => total + Number(cost.amount), 0);
if (totalCost <= 0 || !Number.isFinite(Number(value.targetPrice)) || Number(value.targetPrice) < totalCost) throw new Error("目标售价需不低于单位总成本,且单位总成本必须大于 0");
const riskTypes = ["技术风险", "市场风险", "供应链风险", "资金风险"]; const risks = Array.isArray(value.risks) ? value.risks : [];
if (risks.length !== riskTypes.length || riskTypes.some((type) => !risks.some((risk) => risk?.type === type && String(risk?.description || "").trim() && ["高", "中", "低"].includes(risk?.impact) && String(risk?.response || "").trim()))) throw new Error("请完整填写风险描述、影响程度和应对措施");
const unitGrossProfit = Number(value.targetPrice) - totalCost;
return { valid: true, message: "校验通过", unitTotalCost: totalCost.toFixed(2), unitGrossProfit: unitGrossProfit.toFixed(2), grossMargin: (unitGrossProfit / totalCost * 100).toFixed(2) };
}
export function checkBusinessFeasibilityStepOne(payload) {
if (isStudentDemo()) { try { return Promise.resolve({ code: 200, data: validateBusinessFeasibilityStepOneDemo(payload) }); } catch (error) { return Promise.reject(error); } }
return request({ url: "/api/student-training-answers/business-feasibility/step-1/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}
export function checkBusinessFeasibilityStepTwo(payload) {
const value = payload || {};
if (isStudentDemo()) {
const decisions = ["highly-feasible", "feasible-after-optimization", "observe", "not-feasible"]; const priorities = ["hardware-cost-control", "content-ecosystem", "channel-expansion", "brand-differentiation", "supply-chain-management", "capital-planning"]; const selected = Array.isArray(value.priorities) ? value.priorities : [];
if (!decisions.includes(value.decision)) return Promise.reject(new Error("请选择项目商业可行性总体评估"));
if (selected.some((item) => !priorities.includes(item)) || new Set(selected).size !== selected.length) return Promise.reject(new Error("优先问题包含无效或重复项"));
if (value.decision === "not-feasible" && selected.length) return Promise.reject(new Error("选择不可行时无需勾选优先问题"));
if (value.decision !== "not-feasible" && selected.length !== 3) return Promise.reject(new Error("请且仅选择 3 个需要优先解决的关键问题"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/business-feasibility/step-2/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function checkBusinessFeasibilityStepThree(payload) {
const reportUrl = String(payload?.reportUrl || "").trim(); const reportName = String(payload?.reportName || "").trim(); const supported = /\.(pdf|docx?|png|jpe?g)(?:\?|$)/i;
if (isStudentDemo()) {
if (!reportUrl || !supported.test(reportUrl) && !supported.test(reportName)) return Promise.reject(new Error("请上传 PDF、Word、PNG 或 JPG 格式的可行性论证报告"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/business-feasibility/step-3/validate", method: "post", data: { reportUrl, reportName }, headers: { repeatSubmit: false }, skipErrorMessage: true });
}
export function checkCompetitiveEnvironmentAnalysisStepOne(payload) {
const value = payload || {};
if (isStudentDemo()) {
if (!["strengths", "weaknesses", "opportunities", "threats"].every((key) => String(value[key] || "").trim())) return Promise.reject(new Error("请完整填写优势、劣势、机会和威胁四类信息"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/competitive-environment-analysis/step-1/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function checkCompetitiveEnvironmentAnalysisStepTwo(payload) {
const value = payload || {};
if (isStudentDemo()) {
if (!["strengths", "weaknesses", "opportunities", "threats"].every((key) => String(value[key] || "").trim())) return Promise.reject(new Error("请为优势、劣势、机会和威胁各补充至少 1 条信息"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/competitive-environment-analysis/step-2/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function checkCompetitiveEnvironmentAnalysisStepThree(payload) {
const strategies = Array.isArray(payload?.strategies) ? payload.strategies : [];
if (isStudentDemo()) {
const types = ["so", "wo", "st", "wt"];
if (strategies.length !== types.length || types.some((type) => !strategies.some((item) => item?.strategyType === type)) || strategies.some((item) => !String(item?.recommendation || "").trim() || !String(item?.expectedEffect || "").trim())) return Promise.reject(new Error("请完成 SO、WO、ST 和 WT 四类策略及预期效果"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/competitive-environment-analysis/step-3/validate", method: "post", data: { strategies }, headers: { repeatSubmit: false } });
}
export function checkCompetitiveEnvironmentAnalysisStepFour(payload) {
const value = payload || {}; const priorities = Array.isArray(value.priorities) ? value.priorities : [];
if (isStudentDemo()) {
const types = ["so", "wo", "st", "wt"]; const ranks = priorities.map((item) => Number(item?.priority));
if (priorities.length !== types.length || types.some((type) => !priorities.some((item) => item?.strategyType === type)) || new Set(ranks).size !== 4 || ranks.some((rank) => ![1, 2, 3, 4].includes(rank)) || priorities.some((item) => !String(item?.reason || "").trim()) || !String(value.coreRiskOne || "").trim() || !String(value.coreRiskTwo || "").trim() || !String(value.conclusion || "").trim()) return Promise.reject(new Error("请完整填写策略优先级、至少 2 个核心风险点和总结 SWOT 竞争环境评估结论"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/competitive-environment-analysis/step-4/validate", method: "post", data: value, headers: { repeatSubmit: false }, skipErrorMessage: true });
}
const consumerScenarioJourneyOrder = ["problem-awareness", "information-search", "evaluation-decision", "purchase-conversion", "post-purchase"];
export function checkConsumerScenarioStepOne(payload) {
const stageOrder = Array.isArray(payload?.stageOrder) ? payload.stageOrder : [];
if (isStudentDemo()) return Promise.resolve({ code: 200, data: { valid: stageOrder.length === consumerScenarioJourneyOrder.length && stageOrder.every((item, index) => item === consumerScenarioJourneyOrder[index]), message: "排序校验完成" } });
return request({ url: "/api/student-training-answers/consumer-scenario/step-1/validate", method: "post", data: { stageOrder }, headers: { repeatSubmit: false } });
}
export function checkConsumerScenarioStepTwo(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) { const valid = rows.length === consumerScenarioJourneyOrder.length && rows.every((row) => consumerScenarioJourneyOrder.includes(row?.stageId) && String(row?.behaviorDescription || "").trim() && String(row?.keyTouchpoints || "").trim() && ["负面", "中性", "正面"].includes(row?.emotionExperience) && String(row?.decisionFactors || "").trim()) && new Set(rows.map((row) => row.stageId)).size === consumerScenarioJourneyOrder.length; return Promise.resolve({ code: 200, data: { valid, message: valid ? "校验通过" : "请完整填写五个阶段的行为、触点、情绪和决策因素" } }); }
return request({ url: "/api/student-training-answers/consumer-scenario/step-2/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } });
}
export function checkConsumerScenarioStepThree(payload) {
const journeyMapUrl = String(payload?.journeyMapUrl || "").trim();
if (isStudentDemo()) return Promise.resolve({ code: 200, data: { valid: Boolean(journeyMapUrl), message: journeyMapUrl ? "用户购买旅程图已上传。" : "请上传用户购买旅程图。" } });
return request({ url: "/api/student-training-answers/consumer-scenario/step-3/validate", method: "post", data: { journeyMapUrl }, headers: { repeatSubmit: false } });
}
export function checkConsumerScenarioStepFour(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) { const valid = rows.length === consumerScenarioJourneyOrder.length && rows.every((row) => consumerScenarioJourneyOrder.includes(row?.stageId) && String(row?.painPoint || "").trim() && String(row?.optimizationSuggestion || "").trim()) && new Set(rows.map((row) => row.stageId)).size === consumerScenarioJourneyOrder.length; return Promise.resolve({ code: 200, data: { valid, message: valid ? "五个阶段的优化分析填写完整。" : "请完整填写五个阶段的痛点和优化建议。" } }); }
return request({ url: "/api/student-training-answers/consumer-scenario/step-4/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } });
}
export function checkFeatureConversionStepOne(payload) {
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) { const valid = rows.length >= 8 && rows.length <= 12 && rows.every((row) => ["voice", "insight", "definition", "description", "acceptance"].every((key) => String(row?.[key] || "").trim())); return Promise.resolve({ code: 200, data: { valid, message: valid ? "需求转化表填写完整。" : "请完整填写 8 至 12 行需求转化信息。" } }); }
return request({ url: "/api/student-training-answers/feature-conversion/step-1/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } });
}
export function checkFeatureConversionStepTwo(payload) {
const sourceDefinitions = Array.isArray(payload?.sourceDefinitions) ? payload.sourceDefinitions : []; const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) { const valid = rows.length >= 8 && rows.length <= 12 && sourceDefinitions.length === rows.length && rows.every((row) => ["reach", "impact", "confidence", "effort"].every((key) => Number.isFinite(Number(row?.[key])))); return Promise.resolve({ code: 200, data: { valid, message: valid ? "RICE 评分填写完整。" : "请完整填写 RICE 评分。" } }); }
return request({ url: "/api/student-training-answers/feature-conversion/step-2/validate", method: "post", data: { sourceDefinitions, rows }, headers: { repeatSubmit: false } });
}
export function checkFeatureConversionStepThree(payload) {
const scoreRows = Array.isArray(payload?.scoreRows) ? payload.scoreRows : []; const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (isStudentDemo()) { const valid = scoreRows.length >= 8 && scoreRows.length <= 12 && rows.length === scoreRows.length && rows.every((row, index) => row?.name === scoreRows[index]?.name && ["P0", "P1", "P2", "P3"].includes(row?.priority) && String(row?.reason || "").trim()); return Promise.resolve({ code: 200, data: { valid, message: valid ? "功能优先级填写完整。" : "请完整填写功能优先级与理由。" } }); }
return request({ url: "/api/student-training-answers/feature-conversion/step-3/validate", method: "post", data: { scoreRows, rows }, headers: { repeatSubmit: false } });
}
function checkIndustryDemandDemo(value, required) { if (required.some((key) => !String(value?.[key] || "").trim())) throw new Error("请完整填写 PEST 分析信息"); return { valid: true, message: "校验通过" }; }
export function checkIndustryDemandAnalysisStepOne(payload) { const value = payload || {}; if (isStudentDemo()) { try { return Promise.resolve({ code: 200, data: checkIndustryDemandDemo(value, ["political", "economic", "social", "technology"]) }); } catch (error) { return Promise.reject(error); } } return request({ url: "/api/student-training-answers/industry-demand-analysis/step-1/validate", method: "post", data: value, headers: { repeatSubmit: false } }); }
export function checkIndustryDemandAnalysisStepTwo(payload) { const value = payload || {}; if (isStudentDemo()) { try { return Promise.resolve({ code: 200, data: checkIndustryDemandDemo(value, ["political", "economic", "social", "technology"]) }); } catch (error) { return Promise.reject(error); } } return request({ url: "/api/student-training-answers/industry-demand-analysis/step-2/validate", method: "post", data: value, headers: { repeatSubmit: false } }); }
export function checkIndustryDemandAnalysisStepThree(payload) { const rows = Array.isArray(payload?.rows) ? payload.rows : []; if (isStudentDemo()) { const valid = rows.length === 4 && ["political", "economic", "social", "technology"].every((dimension) => rows.some((row) => row?.dimension === dimension && String(row?.opportunities || "").trim() && String(row?.challenges || "").trim() && String(row?.strategies || "").trim())); return Promise.resolve({ code: 200, data: { valid, message: valid ? "校验通过" : "请完成四个维度的机会、挑战和策略" } }); } return request({ url: "/api/student-training-answers/industry-demand-analysis/step-3/validate", method: "post", data: { rows }, headers: { repeatSubmit: false } }); }
export function checkIndustryDemandAnalysisStepFour(payload) { const value = payload || {}; if (isStudentDemo()) { const valid = ["高度友好", "较为友好", "中性", "存在风险", "风险较高"].includes(String(value.overallAssessment || "").trim()) && ["建议进入", "谨慎进入", "建议暂缓"].includes(String(value.marketDecision || "").trim()) && String(value.decisionReason || "").trim(); return Promise.resolve({ code: 200, data: { valid, message: valid ? "校验通过" : "请完整填写总体评估、市场进入结论和理由" } }); } return request({ url: "/api/student-training-answers/industry-demand-analysis/step-4/validate", method: "post", data: value, headers: { repeatSubmit: false } }); }
export function checkTargetUserProfileStepOne(payload) {
const value = payload || {};
if (isStudentDemo()) {
const fileName = String(value.excelName || "").trim();
const fileUrl = String(value.excelUrl || "").trim();
if (!fileUrl || !/\.xlsx?$/i.test(fileName)) return Promise.reject(new Error("请上传 RFM 用户分层分析结果 Excel 数据"));
if (Number(value.previewRowCount || 0) < 1 || Number(value.previewColumnCount || 0) < 1) return Promise.reject(new Error("Excel 文件需要包含可预览的表头和数据"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/target-user-profile/step-1/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
export function checkTargetUserProfileStepTwo(payload) {
const charts = Array.isArray(payload?.charts) ? payload.charts : [];
if (isStudentDemo()) {
const expectedTypes = ["segment-share", "average-spend", "score-heatmap"];
const types = new Set(charts.map((chart) => chart?.type));
const invalid = charts.find((chart) => !String(chart?.url || "").trim() || !/\.(png|jpe?g|webp)$/i.test(String(chart?.name || "")));
if (charts.length !== expectedTypes.length || expectedTypes.some((type) => !types.has(type)) || invalid) return Promise.reject(new Error("请上传三类 RFM 分析图"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/target-user-profile/step-2/validate", method: "post", data: { charts }, headers: { repeatSubmit: false } });
}
export function checkTargetUserProfileStepThree(payload) {
const insights = Array.isArray(payload?.insights) ? payload.insights : [];
if (isStudentDemo()) {
const expected = ["高价值用户占比", "用户活跃度", "消费能力", "用户忠诚度", "重点客户群特征"];
const dimensions = new Set(insights.map((item) => item?.dimension));
if (insights.length !== expected.length || expected.some((item) => !dimensions.has(item)) || insights.some((item) => !String(item?.analysis || "").trim())) return Promise.reject(new Error("请完成五个 RFM 用户群体特点分析维度"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/target-user-profile/step-3/validate", method: "post", data: { insights }, headers: { repeatSubmit: false } });
}
export function checkTargetUserProfileStepFour(payload) {
const records = Array.isArray(payload?.records) ? payload.records : [];
if (isStudentDemo()) {
const expected = ["U01", "U02", "U03"];
const codes = new Set(records.map((item) => item?.code));
const fields = ["user", "basicAttributes", "tags", "behavior", "painPoints", "psychology"];
if (records.length !== expected.length || expected.some((code) => !codes.has(code)) || records.some((item) => fields.some((field) => !String(item?.[field] || "").trim()))) return Promise.reject(new Error("请完成三位重点用户的完整画像"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/target-user-profile/step-4/validate", method: "post", data: { records }, headers: { repeatSubmit: false } });
}
export function checkTargetUserProfileStepFive(payload) {
const value = payload || {};
const segments = Array.isArray(value.segments) ? value.segments : [];
if (isStudentDemo()) {
const fields = ["market", "attractiveness", "painPoints", "behaviorCharacteristics", "productOpportunity"];
if (!segments.length || segments.length > 6 || !String(value.marketSelection || "").trim() || segments.some((item) => fields.some((field) => !String(item?.[field] || "").trim()))) return Promise.reject(new Error("请完成细分市场分析表和市场选择结论"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/target-user-profile/step-5/validate", method: "post", data: { segments, marketSelection: String(value.marketSelection || "") }, headers: { repeatSubmit: false } });
}