|
|
|
|
@ -64,6 +64,175 @@ export function uploadStudentTrainingFile(data) {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|