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

1756 lines
97 KiB
JavaScript

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 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": "运营难度",
};
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 weights = Array.isArray(payload?.weights) ? payload.weights : [];
const expectedIds = ["search-count", "transaction-amount", "transaction-growth", "category-transaction-growth", "demand-supply-ratio", "online-products", "online-merchants", "organic-traffic-ratio"];
if (isStudentDemo()) {
const ids = new Set(weights.map((item) => item?.id));
const total = weights.reduce((sum, item) => sum + Number(item?.weight || 0), 0);
if (weights.length !== expectedIds.length || expectedIds.some((id) => !ids.has(id)) || weights.some((item) => !Number.isInteger(Number(item?.weight)) || Number(item.weight) < 1 || Number(item.weight) > 100) || total !== 100) {
return Promise.reject(new Error("二级指标权重需为 1 至 100 的整数,且合计必须为 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: { weights }, 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));
if (scores.length !== expectedIds.length || expectedIds.some((id) => !ids.has(id)) || scores.some((item) => !Number.isFinite(Number(item?.score)) || Number(item.score) < 0 || Number(item.score) > 100)) {
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 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 },
});
}
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 },
});
}
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 },
});
}
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 },
});
}
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 },
});
}
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 },
});
}
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 } });
}
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)) || !String(value.overallEvaluation || "").trim() || priorities.some((item) => !String(item?.reason || "").trim()) || !String(value.coreRiskOne || "").trim() || !String(value.coreRiskTwo || "").trim() || !String(value.conclusion || "").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-4/validate", method: "post", data: value, headers: { repeatSubmit: false } });
}
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 } });
}