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