feat: 实现前端组件库和API服务基础架构
refactor: 移除废弃的AGI策略演进服务 fix: 修正磁盘I/O指标字段命名 chore: 更新项目依赖版本 test: 添加前后端集成测试用例 docs: 更新AI模块接口文档 style: 统一审计日志字段命名规范 perf: 优化Redis订阅连接错误处理 build: 配置多项目工作区结构 ci: 添加Vite开发服务器CORS支持
This commit is contained in:
566
server/src/core/ai/ReturnAnalysisService.ts
Normal file
566
server/src/core/ai/ReturnAnalysisService.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Order, Return, Product } from '@prisma/client';
|
||||
import { PrismaService } from '../../config/database';
|
||||
|
||||
interface ReturnData {
|
||||
id: string;
|
||||
orderId: string;
|
||||
productId: string;
|
||||
skuId: string;
|
||||
reason: string;
|
||||
description: string;
|
||||
status: 'PENDING' | 'APPROVED' | 'REJECTED';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface ProductData {
|
||||
id: string;
|
||||
name: string;
|
||||
sku: string;
|
||||
category: string;
|
||||
price: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface ReturnAnalysisResult {
|
||||
skuId: string;
|
||||
productName: string;
|
||||
returnRate: number;
|
||||
totalReturns: number;
|
||||
totalOrders: number;
|
||||
primaryReasons: {
|
||||
reason: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}[];
|
||||
temporalAnalysis: {
|
||||
period: string;
|
||||
returnRate: number;
|
||||
orderCount: number;
|
||||
returnCount: number;
|
||||
}[];
|
||||
recommendations: string[];
|
||||
riskLevel: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReturnAnalysisService {
|
||||
private readonly logger = new Logger(ReturnAnalysisService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* 分析SKU退货原因
|
||||
* @param skuId SKU ID
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 退货分析结果
|
||||
*/
|
||||
async analyzeReturnReasons(skuId: string, traceId: string): Promise<ReturnAnalysisResult> {
|
||||
this.logger.log(`开始分析SKU退货原因: ${skuId}`, { traceId, skuId });
|
||||
|
||||
try {
|
||||
// 获取SKU相关的退货数据
|
||||
const returns = await this.prisma.return.findMany({
|
||||
where: { skuId },
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 获取SKU相关的订单数据
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: {
|
||||
items: {
|
||||
some: {
|
||||
skuId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 获取商品信息
|
||||
const product = await this.prisma.product.findFirst({
|
||||
where: {
|
||||
skus: {
|
||||
some: {
|
||||
id: skuId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new Error(`SKU对应的商品不存在: ${skuId}`);
|
||||
}
|
||||
|
||||
// 计算退货率
|
||||
const totalOrders = orders.length;
|
||||
const totalReturns = returns.length;
|
||||
const returnRate = totalOrders > 0 ? (totalReturns / totalOrders) * 100 : 0;
|
||||
|
||||
// 分析主要退货原因
|
||||
const primaryReasons = this.analyzePrimaryReasons(returns);
|
||||
|
||||
// 时间趋势分析
|
||||
const temporalAnalysis = this.analyzeTemporalTrends(returns, orders);
|
||||
|
||||
// 生成建议
|
||||
const recommendations = this.generateRecommendations({
|
||||
returnRate,
|
||||
primaryReasons,
|
||||
temporalAnalysis,
|
||||
product,
|
||||
});
|
||||
|
||||
// 评估风险等级
|
||||
const riskLevel = this.assessRiskLevel(returnRate);
|
||||
|
||||
const result: ReturnAnalysisResult = {
|
||||
skuId,
|
||||
productName: product.name,
|
||||
returnRate: Math.round(returnRate * 100) / 100,
|
||||
totalReturns,
|
||||
totalOrders,
|
||||
primaryReasons,
|
||||
temporalAnalysis,
|
||||
recommendations,
|
||||
riskLevel,
|
||||
};
|
||||
|
||||
this.logger.log(`SKU退货原因分析完成: ${skuId}, 退货率: ${returnRate}%`, { traceId, result });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`SKU退货原因分析失败: ${error.message}`, { traceId, skuId, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量分析SKU退货原因
|
||||
* @param skuIds SKU ID列表
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 退货分析结果列表
|
||||
*/
|
||||
async batchAnalyzeReturnReasons(skuIds: string[], traceId: string): Promise<ReturnAnalysisResult[]> {
|
||||
this.logger.log(`开始批量分析SKU退货原因, 数量: ${skuIds.length}`, { traceId });
|
||||
|
||||
const results: ReturnAnalysisResult[] = [];
|
||||
|
||||
for (const skuId of skuIds) {
|
||||
try {
|
||||
const result = await this.analyzeReturnReasons(skuId, traceId);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
this.logger.error(`批量分析SKU退货原因失败: ${skuId}, 错误: ${error.message}`, { traceId, skuId });
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`批量分析SKU退货原因完成, 成功: ${results.length}/${skuIds.length}`, { traceId });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高退货率SKU列表
|
||||
* @param threshold 退货率阈值
|
||||
* @param limit 返回数量
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 高退货率SKU列表
|
||||
*/
|
||||
async getHighReturnRateSkus(threshold: number = 10, limit: number = 10, traceId: string): Promise<Array<{
|
||||
skuId: string;
|
||||
productName: string;
|
||||
returnRate: number;
|
||||
totalReturns: number;
|
||||
totalOrders: number;
|
||||
primaryReason: string;
|
||||
}>> {
|
||||
this.logger.log(`获取高退货率SKU列表, 阈值: ${threshold}%, 限制: ${limit}`, { traceId });
|
||||
|
||||
try {
|
||||
// 获取所有SKU的退货数据
|
||||
const returns = await this.prisma.return.findMany({
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 获取所有SKU的订单数据
|
||||
const orders = await this.prisma.order.findMany({
|
||||
include: {
|
||||
items: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 按SKU分组计算退货率
|
||||
const skuReturnStats = new Map<string, { returns: ReturnData[]; orders: Order[] }>();
|
||||
|
||||
// 统计退货
|
||||
returns.forEach(ret => {
|
||||
if (!skuReturnStats.has(ret.skuId)) {
|
||||
skuReturnStats.set(ret.skuId, { returns: [], orders: [] });
|
||||
}
|
||||
skuReturnStats.get(ret.skuId)?.returns.push(ret);
|
||||
});
|
||||
|
||||
// 统计订单
|
||||
orders.forEach(order => {
|
||||
order.items.forEach(item => {
|
||||
if (!skuReturnStats.has(item.skuId)) {
|
||||
skuReturnStats.set(item.skuId, { returns: [], orders: [] });
|
||||
}
|
||||
skuReturnStats.get(item.skuId)?.orders.push(order);
|
||||
});
|
||||
});
|
||||
|
||||
// 计算退货率并筛选高退货率SKU
|
||||
const highReturnRateSkus = [];
|
||||
|
||||
for (const [skuId, stats] of skuReturnStats.entries()) {
|
||||
const totalOrders = stats.orders.length;
|
||||
const totalReturns = stats.returns.length;
|
||||
const returnRate = totalOrders > 0 ? (totalReturns / totalOrders) * 100 : 0;
|
||||
|
||||
if (returnRate >= threshold) {
|
||||
// 获取商品名称
|
||||
const product = await this.prisma.product.findFirst({
|
||||
where: {
|
||||
skus: {
|
||||
some: {
|
||||
id: skuId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 获取主要退货原因
|
||||
const primaryReasons = this.analyzePrimaryReasons(stats.returns);
|
||||
const primaryReason = primaryReasons.length > 0 ? primaryReasons[0].reason : '未知';
|
||||
|
||||
highReturnRateSkus.push({
|
||||
skuId,
|
||||
productName: product?.name || '未知商品',
|
||||
returnRate: Math.round(returnRate * 100) / 100,
|
||||
totalReturns,
|
||||
totalOrders,
|
||||
primaryReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按退货率排序并限制数量
|
||||
highReturnRateSkus.sort((a, b) => b.returnRate - a.returnRate);
|
||||
|
||||
this.logger.log(`获取高退货率SKU列表完成, 数量: ${highReturnRateSkus.length}`, { traceId });
|
||||
|
||||
return highReturnRateSkus.slice(0, limit);
|
||||
} catch (error) {
|
||||
this.logger.error(`获取高退货率SKU列表失败: ${error.message}`, { traceId, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析主要退货原因
|
||||
*/
|
||||
private analyzePrimaryReasons(returns: any[]): ReturnAnalysisResult['primaryReasons'] {
|
||||
// 统计各退货原因的数量
|
||||
const reasonCounts = new Map<string, number>();
|
||||
|
||||
returns.forEach(ret => {
|
||||
const reason = ret.reason || '其他';
|
||||
reasonCounts.set(reason, (reasonCounts.get(reason) || 0) + 1);
|
||||
});
|
||||
|
||||
// 转换为数组并计算百分比
|
||||
const totalReturns = returns.length;
|
||||
const reasons = Array.from(reasonCounts.entries())
|
||||
.map(([reason, count]) => ({
|
||||
reason,
|
||||
count,
|
||||
percentage: totalReturns > 0 ? (count / totalReturns) * 100 : 0,
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 5); // 只返回前5个主要原因
|
||||
|
||||
return reasons;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析时间趋势
|
||||
*/
|
||||
private analyzeTemporalTrends(returns: any[], orders: Order[]): ReturnAnalysisResult['temporalAnalysis'] {
|
||||
// 按周分组分析
|
||||
const weeklyData = new Map<string, { orders: number; returns: number }>();
|
||||
|
||||
// 统计订单
|
||||
orders.forEach(order => {
|
||||
const weekKey = this.getWeekKey(order.createdAt);
|
||||
if (!weeklyData.has(weekKey)) {
|
||||
weeklyData.set(weekKey, { orders: 0, returns: 0 });
|
||||
}
|
||||
weeklyData.get(weekKey)!.orders++;
|
||||
});
|
||||
|
||||
// 统计退货
|
||||
returns.forEach(ret => {
|
||||
const weekKey = this.getWeekKey(ret.createdAt);
|
||||
if (!weeklyData.has(weekKey)) {
|
||||
weeklyData.set(weekKey, { orders: 0, returns: 0 });
|
||||
}
|
||||
weeklyData.get(weekKey)!.returns++;
|
||||
});
|
||||
|
||||
// 转换为数组并计算退货率
|
||||
const temporalData = Array.from(weeklyData.entries())
|
||||
.map(([period, data]) => ({
|
||||
period,
|
||||
returnRate: data.orders > 0 ? (data.returns / data.orders) * 100 : 0,
|
||||
orderCount: data.orders,
|
||||
returnCount: data.returns,
|
||||
}))
|
||||
.sort((a, b) => a.period.localeCompare(b.period));
|
||||
|
||||
return temporalData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取周key
|
||||
*/
|
||||
private getWeekKey(date: Date): string {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const weekNumber = this.getWeekNumber(d);
|
||||
return `${year}-W${weekNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取周数
|
||||
*/
|
||||
private getWeekNumber(date: Date): number {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成建议
|
||||
*/
|
||||
private generateRecommendations(data: {
|
||||
returnRate: number;
|
||||
primaryReasons: ReturnAnalysisResult['primaryReasons'];
|
||||
temporalAnalysis: ReturnAnalysisResult['temporalAnalysis'];
|
||||
product: Product;
|
||||
}): string[] {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
// 基于退货率的建议
|
||||
if (data.returnRate > 20) {
|
||||
recommendations.push('建议考虑暂时下架该SKU,进行全面质量检查');
|
||||
} else if (data.returnRate > 10) {
|
||||
recommendations.push('建议优化产品描述和图片,确保与实际产品一致');
|
||||
}
|
||||
|
||||
// 基于主要退货原因的建议
|
||||
const topReason = data.primaryReasons[0];
|
||||
if (topReason) {
|
||||
switch (topReason.reason) {
|
||||
case '质量问题':
|
||||
recommendations.push('建议加强质量控制,对供应商进行审核');
|
||||
break;
|
||||
case '尺寸不符':
|
||||
recommendations.push('建议提供更详细的尺寸表和测量指南');
|
||||
break;
|
||||
case '描述不符':
|
||||
recommendations.push('建议更新产品描述,确保与实际产品一致');
|
||||
break;
|
||||
case '物流损坏':
|
||||
recommendations.push('建议优化包装,选择更可靠的物流服务商');
|
||||
break;
|
||||
case '其他':
|
||||
recommendations.push('建议收集更详细的退货原因,以便针对性改进');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 基于时间趋势的建议
|
||||
if (data.temporalAnalysis.length > 1) {
|
||||
const recentTrend = this.analyzeTrend(data.temporalAnalysis);
|
||||
if (recentTrend === 'increasing') {
|
||||
recommendations.push('退货率呈上升趋势,建议立即采取措施');
|
||||
} else if (recentTrend === 'decreasing') {
|
||||
recommendations.push('退货率呈下降趋势,建议继续保持当前改进措施');
|
||||
}
|
||||
}
|
||||
|
||||
// 通用建议
|
||||
recommendations.push('建议定期监控该SKU的退货情况,及时调整策略');
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析趋势
|
||||
*/
|
||||
private analyzeTrend(data: ReturnAnalysisResult['temporalAnalysis']): 'increasing' | 'decreasing' | 'stable' {
|
||||
if (data.length < 2) return 'stable';
|
||||
|
||||
const recentData = data.slice(-3); // 取最近3个周期的数据
|
||||
const firstRate = recentData[0].returnRate;
|
||||
const lastRate = recentData[recentData.length - 1].returnRate;
|
||||
|
||||
const changeRate = ((lastRate - firstRate) / firstRate) * 100;
|
||||
|
||||
if (changeRate > 10) {
|
||||
return 'increasing';
|
||||
} else if (changeRate < -10) {
|
||||
return 'decreasing';
|
||||
} else {
|
||||
return 'stable';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估风险等级
|
||||
*/
|
||||
private assessRiskLevel(returnRate: number): 'low' | 'medium' | 'high' {
|
||||
if (returnRate > 20) {
|
||||
return 'high';
|
||||
} else if (returnRate > 10) {
|
||||
return 'medium';
|
||||
} else {
|
||||
return 'low';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成退货分析报告
|
||||
* @param period 时间周期(如:'week', 'month', 'quarter')
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 退货分析报告
|
||||
*/
|
||||
async generateReturnAnalysisReport(period: string, traceId: string) {
|
||||
this.logger.log(`生成退货分析报告, 周期: ${period}`, { traceId, period });
|
||||
|
||||
try {
|
||||
// 计算时间范围
|
||||
const endDate = new Date();
|
||||
let startDate = new Date();
|
||||
|
||||
switch (period) {
|
||||
case 'week':
|
||||
startDate.setDate(startDate.getDate() - 7);
|
||||
break;
|
||||
case 'month':
|
||||
startDate.setMonth(startDate.getMonth() - 1);
|
||||
break;
|
||||
case 'quarter':
|
||||
startDate.setMonth(startDate.getMonth() - 3);
|
||||
break;
|
||||
default:
|
||||
startDate.setMonth(startDate.getMonth() - 1);
|
||||
}
|
||||
|
||||
// 获取时间范围内的退货数据
|
||||
const returns = await this.prisma.return.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 按SKU分组
|
||||
const skuGroups = new Map<string, typeof returns>();
|
||||
returns.forEach(ret => {
|
||||
if (!skuGroups.has(ret.skuId)) {
|
||||
skuGroups.set(ret.skuId, []);
|
||||
}
|
||||
skuGroups.get(ret.skuId)?.push(ret);
|
||||
});
|
||||
|
||||
// 分析每个SKU
|
||||
const skuAnalyses = [];
|
||||
for (const [skuId, skuReturns] of skuGroups.entries()) {
|
||||
try {
|
||||
const analysis = await this.analyzeReturnReasons(skuId, traceId);
|
||||
skuAnalyses.push(analysis);
|
||||
} catch (error) {
|
||||
this.logger.error(`分析SKU ${skuId} 失败: ${error.message}`, { traceId, skuId });
|
||||
}
|
||||
}
|
||||
|
||||
// 计算整体退货率
|
||||
const totalReturns = returns.length;
|
||||
const totalOrders = await this.prisma.order.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
const overallReturnRate = totalOrders > 0 ? (totalReturns / totalOrders) * 100 : 0;
|
||||
|
||||
// 分析主要退货原因
|
||||
const overallReasons = this.analyzePrimaryReasons(returns);
|
||||
|
||||
// 按风险等级分组
|
||||
const highRiskSkus = skuAnalyses.filter(analysis => analysis.riskLevel === 'high');
|
||||
const mediumRiskSkus = skuAnalyses.filter(analysis => analysis.riskLevel === 'medium');
|
||||
const lowRiskSkus = skuAnalyses.filter(analysis => analysis.riskLevel === 'low');
|
||||
|
||||
const report = {
|
||||
period,
|
||||
timeRange: {
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
},
|
||||
overallReturnRate: Math.round(overallReturnRate * 100) / 100,
|
||||
totalReturns,
|
||||
totalOrders,
|
||||
overallReasons,
|
||||
skuAnalyses,
|
||||
riskDistribution: {
|
||||
high: highRiskSkus.length,
|
||||
medium: mediumRiskSkus.length,
|
||||
low: lowRiskSkus.length,
|
||||
},
|
||||
highRiskSkus: highRiskSkus.map(sku => ({
|
||||
skuId: sku.skuId,
|
||||
productName: sku.productName,
|
||||
returnRate: sku.returnRate,
|
||||
primaryReason: sku.primaryReasons[0]?.reason || '未知',
|
||||
})),
|
||||
};
|
||||
|
||||
this.logger.log(`生成退货分析报告完成`, { traceId, report });
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
this.logger.error(`生成退货分析报告失败: ${error.message}`, { traceId, period, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
510
server/src/core/ai/ReturnOptimizationService.ts
Normal file
510
server/src/core/ai/ReturnOptimizationService.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Product, Sku, Return, Order } from '@prisma/client';
|
||||
import { PrismaService } from '../../config/database';
|
||||
|
||||
interface SkuData {
|
||||
id: string;
|
||||
productId: string;
|
||||
sku: string;
|
||||
price: number;
|
||||
stock: number;
|
||||
attributes: Record<string, string>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface ProductData {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
brand: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface OptimizationResult {
|
||||
skuId: string;
|
||||
productName: string;
|
||||
currentReturnRate: number;
|
||||
targetReturnRate: number;
|
||||
optimizationStrategies: {
|
||||
type: string;
|
||||
description: string;
|
||||
expectedImpact: number; // 预期降低退货率的百分比
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
implementationSteps: string[];
|
||||
}[];
|
||||
estimatedSavings: {
|
||||
returnReduction: number; // 预计减少的退货数量
|
||||
revenueImpact: number; // 预计增加的收入
|
||||
costSavings: number; // 预计节省的成本
|
||||
};
|
||||
confidence: number; // 优化方案的置信度
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReturnOptimizationService {
|
||||
private readonly logger = new Logger(ReturnOptimizationService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* 为SKU生成智能优化建议
|
||||
* @param skuId SKU ID
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 优化建议结果
|
||||
*/
|
||||
async generateOptimizationSuggestions(skuId: string, traceId: string): Promise<OptimizationResult> {
|
||||
this.logger.log(`开始为SKU生成优化建议: ${skuId}`, { traceId, skuId });
|
||||
|
||||
try {
|
||||
// 获取SKU信息
|
||||
const sku = await this.prisma.sku.findUnique({
|
||||
where: { id: skuId },
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!sku) {
|
||||
throw new Error(`SKU不存在: ${skuId}`);
|
||||
}
|
||||
|
||||
// 获取SKU的退货数据
|
||||
const returns = await this.prisma.return.findMany({
|
||||
where: { skuId },
|
||||
});
|
||||
|
||||
// 获取SKU的订单数据
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: {
|
||||
items: {
|
||||
some: {
|
||||
skuId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 计算当前退货率
|
||||
const totalOrders = orders.length;
|
||||
const totalReturns = returns.length;
|
||||
const currentReturnRate = totalOrders > 0 ? (totalReturns / totalOrders) * 100 : 0;
|
||||
|
||||
// 分析退货原因
|
||||
const returnReasons = this.analyzeReturnReasons(returns);
|
||||
|
||||
// 生成优化策略
|
||||
const optimizationStrategies = this.generateOptimizationStrategies({
|
||||
sku,
|
||||
product: sku.product,
|
||||
returnReasons,
|
||||
currentReturnRate,
|
||||
});
|
||||
|
||||
// 计算预计节省
|
||||
const estimatedSavings = this.calculateEstimatedSavings({
|
||||
sku,
|
||||
currentReturnRate,
|
||||
optimizationStrategies,
|
||||
orders,
|
||||
});
|
||||
|
||||
// 计算置信度
|
||||
const confidence = this.calculateConfidence(optimizationStrategies, returnReasons);
|
||||
|
||||
// 设置目标退货率
|
||||
const targetReturnRate = Math.max(0, currentReturnRate - 5); // 目标降低5个百分点
|
||||
|
||||
const result: OptimizationResult = {
|
||||
skuId,
|
||||
productName: sku.product.name,
|
||||
currentReturnRate: Math.round(currentReturnRate * 100) / 100,
|
||||
targetReturnRate: Math.round(targetReturnRate * 100) / 100,
|
||||
optimizationStrategies,
|
||||
estimatedSavings,
|
||||
confidence,
|
||||
};
|
||||
|
||||
this.logger.log(`SKU优化建议生成完成: ${skuId}`, { traceId, result });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`SKU优化建议生成失败: ${error.message}`, { traceId, skuId, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量为SKU生成优化建议
|
||||
* @param skuIds SKU ID列表
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 优化建议结果列表
|
||||
*/
|
||||
async batchGenerateOptimizationSuggestions(skuIds: string[], traceId: string): Promise<OptimizationResult[]> {
|
||||
this.logger.log(`开始批量为SKU生成优化建议, 数量: ${skuIds.length}`, { traceId });
|
||||
|
||||
const results: OptimizationResult[] = [];
|
||||
|
||||
for (const skuId of skuIds) {
|
||||
try {
|
||||
const result = await this.generateOptimizationSuggestions(skuId, traceId);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
this.logger.error(`批量生成SKU优化建议失败: ${skuId}, 错误: ${error.message}`, { traceId, skuId });
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`批量生成SKU优化建议完成, 成功: ${results.length}/${skuIds.length}`, { traceId });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析退货原因
|
||||
*/
|
||||
private analyzeReturnReasons(returns: Return[]): Record<string, number> {
|
||||
const reasonCounts = new Map<string, number>();
|
||||
|
||||
returns.forEach(ret => {
|
||||
const reason = ret.reason || '其他';
|
||||
reasonCounts.set(reason, (reasonCounts.get(reason) || 0) + 1);
|
||||
});
|
||||
|
||||
return Object.fromEntries(reasonCounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成优化策略
|
||||
*/
|
||||
private generateOptimizationStrategies(data: {
|
||||
sku: Sku & { product: Product };
|
||||
product: Product;
|
||||
returnReasons: Record<string, number>;
|
||||
currentReturnRate: number;
|
||||
}): OptimizationResult['optimizationStrategies'] {
|
||||
const strategies: OptimizationResult['optimizationStrategies'] = [];
|
||||
|
||||
// 基于退货原因的策略
|
||||
const topReasons = Object.entries(data.returnReasons)
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.slice(0, 3); // 取前3个主要原因
|
||||
|
||||
topReasons.forEach(([reason, count]) => {
|
||||
switch (reason) {
|
||||
case '质量问题':
|
||||
strategies.push({
|
||||
type: 'quality_improvement',
|
||||
description: '改进产品质量,加强质量控制',
|
||||
expectedImpact: 15,
|
||||
priority: 'high',
|
||||
implementationSteps: [
|
||||
'对供应商进行审核和评估',
|
||||
'建立质量检测流程',
|
||||
'对不合格产品进行召回',
|
||||
'定期抽样检查',
|
||||
],
|
||||
});
|
||||
break;
|
||||
case '尺寸不符':
|
||||
strategies.push({
|
||||
type: 'size_accuracy',
|
||||
description: '提供更准确的尺寸信息和测量指南',
|
||||
expectedImpact: 10,
|
||||
priority: 'medium',
|
||||
implementationSteps: [
|
||||
'更新产品页面的尺寸表',
|
||||
'提供详细的测量方法',
|
||||
'添加尺寸对比图表',
|
||||
'收集客户反馈优化尺寸描述',
|
||||
],
|
||||
});
|
||||
break;
|
||||
case '描述不符':
|
||||
strategies.push({
|
||||
type: 'description_accuracy',
|
||||
description: '优化产品描述和图片,确保与实际产品一致',
|
||||
expectedImpact: 12,
|
||||
priority: 'high',
|
||||
implementationSteps: [
|
||||
'更新产品描述,确保准确性',
|
||||
'添加更多真实产品图片',
|
||||
'拍摄产品使用视频',
|
||||
'明确标注产品的材质和特性',
|
||||
],
|
||||
});
|
||||
break;
|
||||
case '物流损坏':
|
||||
strategies.push({
|
||||
type: 'packaging_improvement',
|
||||
description: '优化包装,减少物流损坏',
|
||||
expectedImpact: 8,
|
||||
priority: 'medium',
|
||||
implementationSteps: [
|
||||
'使用更坚固的包装材料',
|
||||
'添加缓冲材料',
|
||||
'选择更可靠的物流服务商',
|
||||
'在包装上标注易碎标志',
|
||||
],
|
||||
});
|
||||
break;
|
||||
case '其他':
|
||||
strategies.push({
|
||||
type: 'customer_feedback',
|
||||
description: '收集更详细的客户反馈,了解具体问题',
|
||||
expectedImpact: 5,
|
||||
priority: 'low',
|
||||
implementationSteps: [
|
||||
'添加详细的退货原因调查',
|
||||
'主动联系退货客户了解原因',
|
||||
'建立客户反馈收集系统',
|
||||
'定期分析反馈数据',
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 通用优化策略
|
||||
if (data.currentReturnRate > 15) {
|
||||
strategies.push({
|
||||
type: 'pricing_strategy',
|
||||
description: '调整定价策略,提高产品价值感知',
|
||||
expectedImpact: 7,
|
||||
priority: 'medium',
|
||||
implementationSteps: [
|
||||
'分析竞争对手定价',
|
||||
'调整产品定价',
|
||||
'提供捆绑销售选项',
|
||||
'推出限时优惠活动',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 产品信息优化
|
||||
strategies.push({
|
||||
type: 'product_information',
|
||||
description: '优化产品页面信息,提高透明度',
|
||||
expectedImpact: 6,
|
||||
priority: 'low',
|
||||
implementationSteps: [
|
||||
'添加详细的产品规格',
|
||||
'提供真实的客户评价',
|
||||
'更新产品使用说明',
|
||||
'添加常见问题解答',
|
||||
],
|
||||
});
|
||||
|
||||
// 客户服务优化
|
||||
strategies.push({
|
||||
type: 'customer_service',
|
||||
description: '提升客户服务质量,减少沟通误解',
|
||||
expectedImpact: 4,
|
||||
priority: 'low',
|
||||
implementationSteps: [
|
||||
'提供更及时的客户支持',
|
||||
'培训客服人员产品知识',
|
||||
'建立快速响应机制',
|
||||
'主动解决客户问题',
|
||||
],
|
||||
});
|
||||
|
||||
// 按优先级排序
|
||||
strategies.sort((a, b) => {
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
});
|
||||
|
||||
return strategies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算预计节省
|
||||
*/
|
||||
private calculateEstimatedSavings(data: {
|
||||
sku: Sku;
|
||||
currentReturnRate: number;
|
||||
optimizationStrategies: OptimizationResult['optimizationStrategies'];
|
||||
orders: Order[];
|
||||
}): OptimizationResult['estimatedSavings'] {
|
||||
// 计算总预期影响
|
||||
const totalExpectedImpact = data.optimizationStrategies
|
||||
.reduce((sum, strategy) => sum + strategy.expectedImpact, 0);
|
||||
|
||||
// 计算预计减少的退货率
|
||||
const expectedReturnRateReduction = Math.min(totalExpectedImpact, data.currentReturnRate);
|
||||
|
||||
// 计算预计减少的退货数量
|
||||
const averageMonthlyOrders = orders.length / 3; // 假设数据覆盖3个月
|
||||
const returnReduction = (averageMonthlyOrders * expectedReturnRateReduction) / 100;
|
||||
|
||||
// 计算预计增加的收入(假设退货商品无法再次销售)
|
||||
const revenueImpact = returnReduction * data.sku.price;
|
||||
|
||||
// 计算预计节省的成本(退货处理成本)
|
||||
const averageReturnProcessingCost = 20; // 假设每笔退货处理成本为20元
|
||||
const costSavings = returnReduction * averageReturnProcessingCost;
|
||||
|
||||
return {
|
||||
returnReduction: Math.round(returnReduction),
|
||||
revenueImpact: Math.round(revenueImpact * 100) / 100,
|
||||
costSavings: Math.round(costSavings * 100) / 100,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算置信度
|
||||
*/
|
||||
private calculateConfidence(
|
||||
strategies: OptimizationResult['optimizationStrategies'],
|
||||
returnReasons: Record<string, number>
|
||||
): number {
|
||||
// 基于策略数量和退货原因分析的完整性计算置信度
|
||||
const strategyCount = strategies.length;
|
||||
const reasonCount = Object.keys(returnReasons).length;
|
||||
|
||||
// 基础置信度
|
||||
let baseConfidence = 70;
|
||||
|
||||
// 根据策略数量调整
|
||||
if (strategyCount >= 5) {
|
||||
baseConfidence += 10;
|
||||
} else if (strategyCount >= 3) {
|
||||
baseConfidence += 5;
|
||||
}
|
||||
|
||||
// 根据退货原因分析调整
|
||||
if (reasonCount >= 3) {
|
||||
baseConfidence += 10;
|
||||
} else if (reasonCount >= 2) {
|
||||
baseConfidence += 5;
|
||||
}
|
||||
|
||||
// 最高置信度为95%
|
||||
return Math.min(95, baseConfidence);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取优化建议报告
|
||||
* @param skuIds SKU ID列表
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 优化建议报告
|
||||
*/
|
||||
async getOptimizationReport(skuIds: string[], traceId: string) {
|
||||
this.logger.log(`生成优化建议报告, SKU数量: ${skuIds.length}`, { traceId });
|
||||
|
||||
try {
|
||||
// 批量生成优化建议
|
||||
const optimizationResults = await this.batchGenerateOptimizationSuggestions(skuIds, traceId);
|
||||
|
||||
// 计算整体统计数据
|
||||
const totalCurrentReturnRate = optimizationResults.reduce((sum, result) => sum + result.currentReturnRate, 0) / optimizationResults.length;
|
||||
const totalTargetReturnRate = optimizationResults.reduce((sum, result) => sum + result.targetReturnRate, 0) / optimizationResults.length;
|
||||
const totalReturnReduction = optimizationResults.reduce((sum, result) => sum + result.estimatedSavings.returnReduction, 0);
|
||||
const totalRevenueImpact = optimizationResults.reduce((sum, result) => sum + result.estimatedSavings.revenueImpact, 0);
|
||||
const totalCostSavings = optimizationResults.reduce((sum, result) => sum + result.estimatedSavings.costSavings, 0);
|
||||
|
||||
// 分析策略分布
|
||||
const strategyDistribution = new Map<string, number>();
|
||||
optimizationResults.forEach(result => {
|
||||
result.optimizationStrategies.forEach(strategy => {
|
||||
strategyDistribution.set(strategy.type, (strategyDistribution.get(strategy.type) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
// 按优先级统计策略
|
||||
const priorityDistribution = new Map<string, number>();
|
||||
optimizationResults.forEach(result => {
|
||||
result.optimizationStrategies.forEach(strategy => {
|
||||
priorityDistribution.set(strategy.priority, (priorityDistribution.get(strategy.priority) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
const report = {
|
||||
skuCount: optimizationResults.length,
|
||||
averageCurrentReturnRate: Math.round(totalCurrentReturnRate * 100) / 100,
|
||||
averageTargetReturnRate: Math.round(totalTargetReturnRate * 100) / 100,
|
||||
totalReturnReduction: Math.round(totalReturnReduction),
|
||||
totalRevenueImpact: Math.round(totalRevenueImpact * 100) / 100,
|
||||
totalCostSavings: Math.round(totalCostSavings * 100) / 100,
|
||||
strategyDistribution: Object.fromEntries(strategyDistribution),
|
||||
priorityDistribution: Object.fromEntries(priorityDistribution),
|
||||
optimizationResults,
|
||||
topRecommendations: this.getTopRecommendations(optimizationResults),
|
||||
};
|
||||
|
||||
this.logger.log(`生成优化建议报告完成`, { traceId, report });
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
this.logger.error(`生成优化建议报告失败: ${error.message}`, { traceId, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶级建议
|
||||
*/
|
||||
private getTopRecommendations(optimizationResults: OptimizationResult[]): Array<{
|
||||
strategyType: string;
|
||||
description: string;
|
||||
frequency: number;
|
||||
averageImpact: number;
|
||||
}> {
|
||||
const strategyStats = new Map<string, { count: number; totalImpact: number; description: string }>();
|
||||
|
||||
optimizationResults.forEach(result => {
|
||||
result.optimizationStrategies.forEach(strategy => {
|
||||
if (!strategyStats.has(strategy.type)) {
|
||||
strategyStats.set(strategy.type, { count: 0, totalImpact: 0, description: strategy.description });
|
||||
}
|
||||
const stats = strategyStats.get(strategy.type)!;
|
||||
stats.count++;
|
||||
stats.totalImpact += strategy.expectedImpact;
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(strategyStats.entries())
|
||||
.map(([type, stats]) => ({
|
||||
strategyType: type,
|
||||
description: stats.description,
|
||||
frequency: stats.count,
|
||||
averageImpact: Math.round((stats.totalImpact / stats.count) * 100) / 100,
|
||||
}))
|
||||
.sort((a, b) => b.frequency - a.frequency)
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用优化策略
|
||||
* @param skuId SKU ID
|
||||
* @param strategyType 策略类型
|
||||
* @param traceId 链路追踪ID
|
||||
* @returns 应用结果
|
||||
*/
|
||||
async applyOptimizationStrategy(skuId: string, strategyType: string, traceId: string) {
|
||||
this.logger.log(`应用优化策略: ${strategyType} 到 SKU: ${skuId}`, { traceId, skuId, strategyType });
|
||||
|
||||
try {
|
||||
// 这里可以实现具体的策略应用逻辑
|
||||
// 例如:更新产品信息、调整定价、优化包装等
|
||||
|
||||
// 模拟应用结果
|
||||
const result = {
|
||||
skuId,
|
||||
strategyType,
|
||||
applied: true,
|
||||
message: `成功应用 ${strategyType} 策略到 SKU ${skuId}`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
this.logger.log(`优化策略应用完成`, { traceId, result });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`应用优化策略失败: ${error.message}`, { traceId, skuId, strategyType, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { logger } from '../../utils/logger';
|
||||
import { FeatureGovernanceService } from '../governance/FeatureGovernanceService';
|
||||
import db from '../../config/database';
|
||||
|
||||
export interface ComputeJob {
|
||||
jobId: string;
|
||||
tenantId: string;
|
||||
complexity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
allocatedNodes: number;
|
||||
cpuLimit: number; // mCore
|
||||
memoryLimit: number; // MiB
|
||||
status: 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED';
|
||||
}
|
||||
|
||||
/**
|
||||
* [CORE_DEV_35] AGI 弹性算力动态编排 V2 (Elastic Compute V2)
|
||||
* @description 核心逻辑:针对多 Agent 协作产生的海量、异构算力需求,实现亚秒级的资源调度。
|
||||
* 系统根据 AGI 任务的认知复杂度(如:简单的 SKU 翻译 vs 复杂的全球供应链 RCA)
|
||||
* 动态调整算力配额,支持在 K8s/Edge 节点间执行“计算热迁移”,确保关键决策链路的零延迟响应。
|
||||
*/
|
||||
export class ElasticComputeServiceV2 {
|
||||
private static readonly JOBS_TABLE = 'cf_agi_compute_jobs';
|
||||
|
||||
/**
|
||||
* 初始化表结构
|
||||
*/
|
||||
static async initTable() {
|
||||
const hasTable = await db.schema.hasTable(this.JOBS_TABLE);
|
||||
if (!hasTable) {
|
||||
console.log(`📦 Creating ${this.JOBS_TABLE} table...`);
|
||||
await db.schema.createTable(this.JOBS_TABLE, (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('job_id', 64).notNullable().unique();
|
||||
table.string('tenant_id', 64).notNullable();
|
||||
table.string('complexity', 16);
|
||||
table.integer('allocated_nodes');
|
||||
table.integer('cpu_limit');
|
||||
table.integer('memory_limit');
|
||||
table.string('status', 16).defaultTo('QUEUED');
|
||||
table.timestamp('created_at').defaultTo(db.fn.now());
|
||||
table.index(['tenant_id', 'status']);
|
||||
});
|
||||
console.log(`✅ Table ${this.JOBS_TABLE} created`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请算力资源并启动任务
|
||||
*/
|
||||
static async scheduleAGIJob(params: {
|
||||
tenantId: string;
|
||||
complexity: ComputeJob['complexity'];
|
||||
}): Promise<string> {
|
||||
const jobId = `AGI-JOB-${Date.now()}`;
|
||||
|
||||
// 1. 动态资源估算逻辑
|
||||
const config = this.estimateResourceNeeds(params.complexity);
|
||||
|
||||
await db(this.JOBS_TABLE).insert({
|
||||
job_id: jobId,
|
||||
tenant_id: params.tenantId,
|
||||
complexity: params.complexity,
|
||||
allocated_nodes: config.nodes,
|
||||
cpu_limit: config.cpu,
|
||||
memory_limit: config.memory,
|
||||
status: 'RUNNING'
|
||||
});
|
||||
|
||||
logger.info(`[ElasticCompute] Scheduled ${params.complexity} job ${jobId} with ${config.nodes} nodes for Tenant ${params.tenantId}`);
|
||||
|
||||
// 2. 联动边缘 WASM 或 TEE 集群进行容器启动 (模拟)
|
||||
// await InfinityComputeService.provisionNodes(jobId, config);
|
||||
|
||||
return jobId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源需求估算模型
|
||||
*/
|
||||
private static estimateResourceNeeds(complexity: ComputeJob['complexity']) {
|
||||
switch (complexity) {
|
||||
case 'CRITICAL': return { nodes: 16, cpu: 8000, memory: 32768 };
|
||||
case 'HIGH': return { nodes: 4, cpu: 2000, memory: 8192 };
|
||||
case 'MEDIUM': return { nodes: 2, cpu: 1000, memory: 4096 };
|
||||
default: return { nodes: 1, cpu: 500, memory: 1024 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放算力资源
|
||||
*/
|
||||
static async completeJob(jobId: string) {
|
||||
await db(this.JOBS_TABLE).where({ job_id: jobId }).update({ status: 'COMPLETED' });
|
||||
logger.info(`[ElasticCompute] Resources released for job ${jobId}`);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { FeatureToggleService } from '../governance/FeatureToggleService';
|
||||
import { S3QuotaManager } from '../governance/S3QuotaManager';
|
||||
|
||||
// Business Services
|
||||
import { AGIStrategyEvolutionService } from '../../services/AGIStrategyEvolutionService';
|
||||
import { ActionAuditService } from '../../services/ActionAuditService';
|
||||
import { AgentSwarmService } from '../../services/AgentSwarmService';
|
||||
import { AutoCircuitBreakerService } from '../../services/AutoCircuitBreakerService';
|
||||
@@ -539,11 +538,6 @@ export class DomainBootstrap {
|
||||
priority: DomainRegistry.Priority.CORE_INFRA,
|
||||
init: () => ExperimentService.initTable()
|
||||
});
|
||||
DomainRegistry.register({
|
||||
name: 'AGIStrategyEvolution',
|
||||
priority: DomainRegistry.Priority.CORE_INFRA,
|
||||
init: () => AGIStrategyEvolutionService.initTable()
|
||||
});
|
||||
|
||||
// 2. 运行时与安全 (SECURITY / RUNTIME)
|
||||
DomainRegistry.register({
|
||||
|
||||
@@ -86,7 +86,6 @@ import { TaxReportService } from '../../services/TaxReportService';
|
||||
import { UnifiedTaskService } from '../../services/UnifiedTaskService';
|
||||
import { AgentSelfAwarenessService } from '../ai/AgentSelfAwarenessService';
|
||||
import { AGILegalComplianceService } from '../ai/AGILegalComplianceService';
|
||||
import { AGIStrategyEvolutionService } from '../ai/AGIStrategyEvolutionService';
|
||||
import { AutoCircuitBreakerService } from '../ai/AutoCircuitBreakerService';
|
||||
import { FederatedNodeService } from '../ai/FederatedNodeService';
|
||||
import { InfinityComputeService } from '../ai/InfinityComputeService';
|
||||
@@ -204,7 +203,6 @@ export class LegacyTableInitializer {
|
||||
await SKUMappingService.initTable();
|
||||
await UnifiedTaskService.initTable();
|
||||
await SummaryAggregationService.initTable();
|
||||
await AGIStrategyEvolutionService.initTable();
|
||||
await FederatedNodeService.initTable();
|
||||
await AutonomousSandboxService.initTable();
|
||||
await SandboxROIAdvisor.initTable();
|
||||
|
||||
Reference in New Issue
Block a user