feat: 实现前端组件库和API服务基础架构
refactor: 移除废弃的AGI策略演进服务 fix: 修正磁盘I/O指标字段命名 chore: 更新项目依赖版本 test: 添加前后端集成测试用例 docs: 更新AI模块接口文档 style: 统一审计日志字段命名规范 perf: 优化Redis订阅连接错误处理 build: 配置多项目工作区结构 ci: 添加Vite开发服务器CORS支持
This commit is contained in:
463
server/src/core/data/ReturnEffectAnalysisService.ts
Normal file
463
server/src/core/data/ReturnEffectAnalysisService.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Logger } from 'winston';
|
||||
import { ReturnEffectAnalysisResult, ReturnEffectMetrics, SKUImpactAnalysis } from '../../shared/types/return';
|
||||
|
||||
/**
|
||||
* 退货效果分析服务
|
||||
* 分析高退货率SKU对销售、利润的影响,生成分析报告
|
||||
*/
|
||||
export class ReturnEffectAnalysisService {
|
||||
private prisma: PrismaClient;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(prisma: PrismaClient, logger: Logger) {
|
||||
this.prisma = prisma;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析退货对SKU的影响
|
||||
* @param skuId SKU ID
|
||||
* @param timeRange 时间范围
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 退货效果分析结果
|
||||
*/
|
||||
async analyzeSKUReturnEffect(
|
||||
skuId: string,
|
||||
timeRange: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
},
|
||||
traceId: string
|
||||
): Promise<ReturnEffectAnalysisResult> {
|
||||
this.logger.info(`开始分析SKU ${skuId} 的退货效果`, { traceId });
|
||||
|
||||
try {
|
||||
// 获取SKU信息
|
||||
const sku = await this.prisma.sku.findUnique({
|
||||
where: { id: skuId },
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!sku) {
|
||||
throw new Error(`SKU ${skuId} 不存在`);
|
||||
}
|
||||
|
||||
// 获取退货数据
|
||||
const returns = await this.prisma.return.findMany({
|
||||
where: {
|
||||
skuId,
|
||||
createdAt: {
|
||||
gte: timeRange.start,
|
||||
lte: timeRange.end
|
||||
}
|
||||
},
|
||||
include: {
|
||||
order: true
|
||||
}
|
||||
});
|
||||
|
||||
// 获取销售数据
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: {
|
||||
items: {
|
||||
some: {
|
||||
skuId
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
gte: timeRange.start,
|
||||
lte: timeRange.end
|
||||
}
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
where: {
|
||||
skuId
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 计算基本指标
|
||||
const totalOrders = orders.length;
|
||||
const totalReturns = returns.length;
|
||||
const returnRate = totalOrders > 0 ? (totalReturns / totalOrders) * 100 : 0;
|
||||
|
||||
// 计算销售影响
|
||||
const totalSales = orders.reduce((sum, order) => {
|
||||
const skuItem = order.items.find(item => item.skuId === skuId);
|
||||
return sum + (skuItem ? skuItem.price * skuItem.quantity : 0);
|
||||
}, 0);
|
||||
|
||||
const returnedSales = returns.reduce((sum, returnItem) => {
|
||||
return sum + returnItem.refundAmount;
|
||||
}, 0);
|
||||
|
||||
const salesImpact = (returnedSales / totalSales) * 100;
|
||||
|
||||
// 计算利润影响
|
||||
const totalProfit = orders.reduce((sum, order) => {
|
||||
const skuItem = order.items.find(item => item.skuId === skuId);
|
||||
if (!skuItem) return sum;
|
||||
const cost = sku.costPrice || 0;
|
||||
return sum + (skuItem.price - cost) * skuItem.quantity;
|
||||
}, 0);
|
||||
|
||||
const lostProfit = returns.reduce((sum, returnItem) => {
|
||||
const cost = sku.costPrice || 0;
|
||||
const order = orders.find(o => o.id === returnItem.orderId);
|
||||
if (!order) return sum;
|
||||
const skuItem = order.items.find(item => item.skuId === skuId);
|
||||
if (!skuItem) return sum;
|
||||
return sum + (skuItem.price - cost) * skuItem.quantity;
|
||||
}, 0);
|
||||
|
||||
const profitImpact = (lostProfit / totalProfit) * 100;
|
||||
|
||||
// 分析退货原因分布
|
||||
const reasonDistribution = this.analyzeReasonDistribution(returns);
|
||||
|
||||
// 生成改进建议
|
||||
const recommendations = this.generateRecommendations({
|
||||
returnRate,
|
||||
salesImpact,
|
||||
profitImpact,
|
||||
reasonDistribution,
|
||||
sku
|
||||
});
|
||||
|
||||
// 生成分析报告
|
||||
const analysisReport = this.generateAnalysisReport({
|
||||
sku,
|
||||
timeRange,
|
||||
returnRate,
|
||||
salesImpact,
|
||||
profitImpact,
|
||||
reasonDistribution,
|
||||
recommendations
|
||||
});
|
||||
|
||||
const result: ReturnEffectAnalysisResult = {
|
||||
skuId,
|
||||
productName: sku.product.name,
|
||||
timeRange,
|
||||
metrics: {
|
||||
returnRate,
|
||||
totalOrders,
|
||||
totalReturns,
|
||||
totalSales,
|
||||
returnedSales,
|
||||
salesImpact,
|
||||
totalProfit,
|
||||
lostProfit,
|
||||
profitImpact
|
||||
},
|
||||
reasonDistribution,
|
||||
recommendations,
|
||||
analysisReport
|
||||
};
|
||||
|
||||
this.logger.info(`SKU ${skuId} 退货效果分析完成`, { traceId, returnRate, salesImpact, profitImpact });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`分析SKU ${skuId} 退货效果失败`, { traceId, error: (error as Error).message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量分析多个SKU的退货效果
|
||||
* @param skuIds SKU ID列表
|
||||
* @param timeRange 时间范围
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 多个SKU的退货效果分析结果
|
||||
*/
|
||||
async batchAnalyzeSKUReturnEffect(
|
||||
skuIds: string[],
|
||||
timeRange: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
},
|
||||
traceId: string
|
||||
): Promise<ReturnEffectAnalysisResult[]> {
|
||||
this.logger.info(`开始批量分析 ${skuIds.length} 个SKU的退货效果`, { traceId });
|
||||
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
skuIds.map(skuId => this.analyzeSKUReturnEffect(skuId, timeRange, traceId))
|
||||
);
|
||||
|
||||
this.logger.info(`批量分析完成,成功分析 ${results.length} 个SKU`, { traceId });
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error(`批量分析SKU退货效果失败`, { traceId, error: (error as Error).message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析退货原因分布
|
||||
* @param returns 退货数据
|
||||
* @returns 退货原因分布
|
||||
*/
|
||||
private analyzeReasonDistribution(returns: any[]): Record<string, number> {
|
||||
const distribution: Record<string, number> = {};
|
||||
|
||||
returns.forEach(returnItem => {
|
||||
const reason = returnItem.reason || '其他';
|
||||
distribution[reason] = (distribution[reason] || 0) + 1;
|
||||
});
|
||||
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成改进建议
|
||||
* @param analysisData 分析数据
|
||||
* @returns 改进建议
|
||||
*/
|
||||
private generateRecommendations(analysisData: {
|
||||
returnRate: number;
|
||||
salesImpact: number;
|
||||
profitImpact: number;
|
||||
reasonDistribution: Record<string, number>;
|
||||
sku: any;
|
||||
}): string[] {
|
||||
const { returnRate, salesImpact, profitImpact, reasonDistribution, sku } = analysisData;
|
||||
const recommendations: string[] = [];
|
||||
|
||||
// 根据退货率生成建议
|
||||
if (returnRate > 30) {
|
||||
recommendations.push('考虑暂时下架该SKU,进行全面质量检查');
|
||||
recommendations.push('重新评估供应商,考虑更换供应商');
|
||||
} else if (returnRate > 20) {
|
||||
recommendations.push('加强质量控制,提高产品质量');
|
||||
recommendations.push('优化产品描述,确保与实际产品一致');
|
||||
} else if (returnRate > 10) {
|
||||
recommendations.push('定期检查库存,确保产品存储条件良好');
|
||||
recommendations.push('提供更详细的产品使用说明');
|
||||
}
|
||||
|
||||
// 根据销售影响生成建议
|
||||
if (salesImpact > 20) {
|
||||
recommendations.push('调整定价策略,考虑降价以减少退货率');
|
||||
recommendations.push('加强售前咨询,确保客户了解产品特性');
|
||||
}
|
||||
|
||||
// 根据利润影响生成建议
|
||||
if (profitImpact > 25) {
|
||||
recommendations.push('重新评估成本结构,寻找降低成本的方法');
|
||||
recommendations.push('考虑提高产品价格以抵消退货损失');
|
||||
}
|
||||
|
||||
// 根据退货原因生成建议
|
||||
const topReason = Object.entries(reasonDistribution)
|
||||
.sort(([,a], [,b]) => b - a)[0];
|
||||
|
||||
if (topReason) {
|
||||
const [reason, count] = topReason;
|
||||
switch (reason) {
|
||||
case '质量问题':
|
||||
recommendations.push('加强产品质量检测,确保符合标准');
|
||||
recommendations.push('与供应商沟通,要求提高产品质量');
|
||||
break;
|
||||
case '尺寸不合适':
|
||||
recommendations.push('提供更详细的尺寸图表');
|
||||
recommendations.push('考虑提供尺寸测量指南');
|
||||
break;
|
||||
case '描述不符':
|
||||
recommendations.push('更新产品描述,确保与实际产品一致');
|
||||
recommendations.push('增加产品实拍图片和视频');
|
||||
break;
|
||||
case '物流损坏':
|
||||
recommendations.push('优化包装方式,减少运输损坏');
|
||||
recommendations.push('与物流公司合作,改善运输条件');
|
||||
break;
|
||||
default:
|
||||
recommendations.push(`针对"${reason}"原因,制定相应的改进措施`);
|
||||
}
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成分析报告
|
||||
* @param data 分析数据
|
||||
* @returns 分析报告
|
||||
*/
|
||||
private generateAnalysisReport(data: {
|
||||
sku: any;
|
||||
timeRange: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
returnRate: number;
|
||||
salesImpact: number;
|
||||
profitImpact: number;
|
||||
reasonDistribution: Record<string, number>;
|
||||
recommendations: string[];
|
||||
}): string {
|
||||
const { sku, timeRange, returnRate, salesImpact, profitImpact, reasonDistribution, recommendations } = data;
|
||||
|
||||
let report = `# SKU 退货效果分析报告\n\n`;
|
||||
report += `## 基本信息\n`;
|
||||
report += `- SKU ID: ${sku.id}\n`;
|
||||
report += `- 产品名称: ${sku.product.name}\n`;
|
||||
report += `- 分析时间范围: ${timeRange.start.toISOString()} 至 ${timeRange.end.toISOString()}\n\n`;
|
||||
|
||||
report += `## 核心指标\n`;
|
||||
report += `- 退货率: ${returnRate.toFixed(2)}%\n`;
|
||||
report += `- 销售影响: ${salesImpact.toFixed(2)}%\n`;
|
||||
report += `- 利润影响: ${profitImpact.toFixed(2)}%\n\n`;
|
||||
|
||||
report += `## 退货原因分布\n`;
|
||||
Object.entries(reasonDistribution).forEach(([reason, count]) => {
|
||||
report += `- ${reason}: ${count} 次\n`;
|
||||
});
|
||||
report += `\n`;
|
||||
|
||||
report += `## 改进建议\n`;
|
||||
recommendations.forEach((recommendation, index) => {
|
||||
report += `${index + 1}. ${recommendation}\n`;
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析高退货率SKU的整体影响
|
||||
* @param threshold 退货率阈值
|
||||
* @param timeRange 时间范围
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 高退货率SKU的整体影响分析
|
||||
*/
|
||||
async analyzeHighReturnSKUsImpact(
|
||||
threshold: number,
|
||||
timeRange: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
},
|
||||
traceId: string
|
||||
): Promise<SKUImpactAnalysis> {
|
||||
this.logger.info(`开始分析退货率超过 ${threshold}% 的SKU整体影响`, { traceId });
|
||||
|
||||
try {
|
||||
// 获取所有SKU的退货数据
|
||||
const skus = await this.prisma.sku.findMany({
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
});
|
||||
|
||||
// 分析每个SKU的退货率
|
||||
const highReturnSKUs = [];
|
||||
let totalImpact = 0;
|
||||
|
||||
for (const sku of skus) {
|
||||
const returns = await this.prisma.return.count({
|
||||
where: {
|
||||
skuId: sku.id,
|
||||
createdAt: {
|
||||
gte: timeRange.start,
|
||||
lte: timeRange.end
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const orders = await this.prisma.order.count({
|
||||
where: {
|
||||
items: {
|
||||
some: {
|
||||
skuId: sku.id
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
gte: timeRange.start,
|
||||
lte: timeRange.end
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const returnRate = orders > 0 ? (returns / orders) * 100 : 0;
|
||||
|
||||
if (returnRate > threshold) {
|
||||
// 计算该SKU的销售和利润影响
|
||||
const skuOrders = await this.prisma.order.findMany({
|
||||
where: {
|
||||
items: {
|
||||
some: {
|
||||
skuId: sku.id
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
gte: timeRange.start,
|
||||
lte: timeRange.end
|
||||
}
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
where: {
|
||||
skuId: sku.id
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const totalSales = skuOrders.reduce((sum, order) => {
|
||||
const skuItem = order.items.find(item => item.skuId === sku.id);
|
||||
return sum + (skuItem ? skuItem.price * skuItem.quantity : 0);
|
||||
}, 0);
|
||||
|
||||
const skuReturns = await this.prisma.return.findMany({
|
||||
where: {
|
||||
skuId: sku.id,
|
||||
createdAt: {
|
||||
gte: timeRange.start,
|
||||
lte: timeRange.end
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const returnedSales = skuReturns.reduce((sum, returnItem) => {
|
||||
return sum + returnItem.refundAmount;
|
||||
}, 0);
|
||||
|
||||
const salesImpact = (returnedSales / totalSales) * 100;
|
||||
|
||||
highReturnSKUs.push({
|
||||
skuId: sku.id,
|
||||
productName: sku.product.name,
|
||||
returnRate,
|
||||
salesImpact,
|
||||
totalSales,
|
||||
returnedSales
|
||||
});
|
||||
|
||||
totalImpact += salesImpact;
|
||||
}
|
||||
}
|
||||
|
||||
// 按退货率排序
|
||||
highReturnSKUs.sort((a, b) => b.returnRate - a.returnRate);
|
||||
|
||||
const analysis: SKUImpactAnalysis = {
|
||||
threshold,
|
||||
timeRange,
|
||||
totalHighReturnSKUs: highReturnSKUs.length,
|
||||
averageImpact: highReturnSKUs.length > 0 ? totalImpact / highReturnSKUs.length : 0,
|
||||
highReturnSKUs,
|
||||
top5SKUs: highReturnSKUs.slice(0, 5)
|
||||
};
|
||||
|
||||
this.logger.info(`高退货率SKU整体影响分析完成,共发现 ${highReturnSKUs.length} 个高退货率SKU`, { traceId });
|
||||
return analysis;
|
||||
} catch (error) {
|
||||
this.logger.error(`分析高退货率SKU整体影响失败`, { traceId, error: (error as Error).message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user