refactor: 优化服务层代码并修复类型问题 docs: 更新开发进度文档 feat(merchant): 新增商户监控与数据统计服务 feat(dashboard): 添加商户管理前端页面与服务 fix: 修复类型转换与可选参数处理 feat: 实现商户订单、店铺与结算管理功能 refactor: 重构审计日志格式与服务调用 feat: 新增商户入驻与身份注册功能 fix(controller): 修复路由参数类型问题 feat: 添加商户排名与统计报告功能 chore: 更新模拟数据与服务配置
436 lines
14 KiB
TypeScript
436 lines
14 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
||
import { Settlement, Order, Merchant } from '@prisma/client';
|
||
import { PrismaService } from '../../config/database';
|
||
|
||
interface SettlementData {
|
||
id: string;
|
||
merchantId: string;
|
||
periodStart: Date;
|
||
periodEnd: Date;
|
||
totalAmount: number;
|
||
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
|
||
createdAt: Date;
|
||
updatedAt: Date;
|
||
}
|
||
|
||
interface FeeData {
|
||
feeType: string;
|
||
amount: number;
|
||
description: string;
|
||
merchantId: string;
|
||
createdAt: Date;
|
||
}
|
||
|
||
interface OptimizationResult {
|
||
settlementId: string;
|
||
merchantId: string;
|
||
originalAmount: number;
|
||
optimizedAmount: number;
|
||
savings: number;
|
||
optimizationRate: number;
|
||
recommendations: {
|
||
type: string;
|
||
description: string;
|
||
potentialSavings: number;
|
||
priority: 'high' | 'medium' | 'low';
|
||
}[];
|
||
feeBreakdown: {
|
||
originalFees: FeeData[];
|
||
optimizedFees: FeeData[];
|
||
};
|
||
confidence: number;
|
||
}
|
||
|
||
@Injectable()
|
||
export class SettlementOptimizationService {
|
||
private readonly logger = new Logger(SettlementOptimizationService.name);
|
||
|
||
constructor(private readonly prisma: PrismaService) {}
|
||
|
||
/**
|
||
* 优化商户结算
|
||
* @param settlementId 结算单ID
|
||
* @param traceId 链路追踪ID
|
||
* @returns 结算优化结果
|
||
*/
|
||
async optimizeSettlement(settlementId: string, traceId: string): Promise<OptimizationResult> {
|
||
this.logger.log(`开始优化结算: ${settlementId}`, { traceId, settlementId });
|
||
|
||
try {
|
||
// 获取结算单信息
|
||
const settlement = await this.prisma.settlement.findUnique({
|
||
where: { id: settlementId },
|
||
});
|
||
|
||
if (!settlement) {
|
||
throw new Error(`结算单不存在: ${settlementId}`);
|
||
}
|
||
|
||
// 获取商户信息
|
||
const merchant = await this.prisma.merchant.findUnique({
|
||
where: { id: settlement.merchantId },
|
||
});
|
||
|
||
if (!merchant) {
|
||
throw new Error(`商户不存在: ${settlement.merchantId}`);
|
||
}
|
||
|
||
// 获取结算周期内的订单数据
|
||
const orders = await this.prisma.order.findMany({
|
||
where: {
|
||
merchantId: settlement.merchantId,
|
||
createdAt: {
|
||
gte: settlement.periodStart,
|
||
lte: settlement.periodEnd,
|
||
},
|
||
},
|
||
});
|
||
|
||
// 获取结算周期内的费用数据
|
||
const fees = await this.getFeeData(settlement.merchantId, settlement.periodStart, settlement.periodEnd);
|
||
|
||
// 分析结算数据
|
||
const analysis = this.analyzeSettlementData(settlement, orders, fees);
|
||
|
||
// 生成优化建议
|
||
const recommendations = this.generateOptimizationRecommendations(analysis, merchant);
|
||
|
||
// 计算优化后的金额
|
||
const optimizedAmount = this.calculateOptimizedAmount(settlement.totalAmount, recommendations);
|
||
|
||
// 计算节省金额和优化率
|
||
const savings = settlement.totalAmount - optimizedAmount;
|
||
const optimizationRate = settlement.totalAmount > 0 ? (savings / settlement.totalAmount) * 100 : 0;
|
||
|
||
// 生成优化后的费用明细
|
||
const optimizedFees = this.generateOptimizedFees(fees, recommendations);
|
||
|
||
// 计算置信度
|
||
const confidence = this.calculateConfidence(recommendations);
|
||
|
||
const result: OptimizationResult = {
|
||
settlementId,
|
||
merchantId: settlement.merchantId,
|
||
originalAmount: settlement.totalAmount,
|
||
optimizedAmount,
|
||
savings,
|
||
optimizationRate: Math.round(optimizationRate * 100) / 100,
|
||
recommendations,
|
||
feeBreakdown: {
|
||
originalFees: fees,
|
||
optimizedFees,
|
||
},
|
||
confidence,
|
||
};
|
||
|
||
this.logger.log(`结算优化完成: ${settlementId}, 节省: ${savings}, 优化率: ${optimizationRate}%`, { traceId, result });
|
||
|
||
return result;
|
||
} catch (error) {
|
||
this.logger.error(`结算优化失败: ${error.message}`, { traceId, settlementId, error });
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量优化商户结算
|
||
* @param settlementIds 结算单ID列表
|
||
* @param traceId 链路追踪ID
|
||
* @returns 结算优化结果列表
|
||
*/
|
||
async batchOptimizeSettlements(settlementIds: string[], traceId: string): Promise<OptimizationResult[]> {
|
||
this.logger.log(`开始批量优化结算, 数量: ${settlementIds.length}`, { traceId });
|
||
|
||
const results: OptimizationResult[] = [];
|
||
|
||
for (const settlementId of settlementIds) {
|
||
try {
|
||
const result = await this.optimizeSettlement(settlementId, traceId);
|
||
results.push(result);
|
||
} catch (error) {
|
||
this.logger.error(`批量优化结算失败: ${settlementId}, 错误: ${error.message}`, { traceId, settlementId });
|
||
}
|
||
}
|
||
|
||
this.logger.log(`批量优化结算完成, 成功: ${results.length}/${settlementIds.length}`, { traceId });
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 获取费用数据
|
||
*/
|
||
private async getFeeData(merchantId: string, startDate: Date, endDate: Date): Promise<FeeData[]> {
|
||
// 这里模拟获取费用数据
|
||
// 实际项目中应该从数据库或API获取
|
||
return [
|
||
{
|
||
feeType: 'platform_fee',
|
||
amount: 100.00,
|
||
description: '平台服务费',
|
||
merchantId,
|
||
createdAt: new Date(),
|
||
},
|
||
{
|
||
feeType: 'transaction_fee',
|
||
amount: 50.00,
|
||
description: '交易手续费',
|
||
merchantId,
|
||
createdAt: new Date(),
|
||
},
|
||
{
|
||
feeType: 'logistics_fee',
|
||
amount: 80.00,
|
||
description: '物流费用',
|
||
merchantId,
|
||
createdAt: new Date(),
|
||
},
|
||
{
|
||
feeType: 'marketing_fee',
|
||
amount: 30.00,
|
||
description: '营销费用',
|
||
merchantId,
|
||
createdAt: new Date(),
|
||
},
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 分析结算数据
|
||
*/
|
||
private analyzeSettlementData(settlement: SettlementData, orders: Order[], fees: FeeData[]) {
|
||
// 计算订单相关指标
|
||
const orderCount = orders.length;
|
||
const totalOrderAmount = orders.reduce((sum, order) => sum + order.totalAmount, 0);
|
||
const averageOrderValue = orderCount > 0 ? totalOrderAmount / orderCount : 0;
|
||
|
||
// 计算费用相关指标
|
||
const totalFees = fees.reduce((sum, fee) => sum + fee.amount, 0);
|
||
const feeToSalesRatio = totalOrderAmount > 0 ? (totalFees / totalOrderAmount) * 100 : 0;
|
||
|
||
// 分析费用结构
|
||
const feeStructure = fees.reduce((acc, fee) => {
|
||
acc[fee.feeType] = (acc[fee.feeType] || 0) + fee.amount;
|
||
return acc;
|
||
}, {} as Record<string, number>);
|
||
|
||
return {
|
||
orderCount,
|
||
totalOrderAmount,
|
||
averageOrderValue,
|
||
totalFees,
|
||
feeToSalesRatio,
|
||
feeStructure,
|
||
settlementAmount: settlement.totalAmount,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 生成优化建议
|
||
*/
|
||
private generateOptimizationRecommendations(analysis: any, merchant: Merchant) {
|
||
const recommendations: OptimizationResult['recommendations'] = [];
|
||
|
||
// 平台服务费优化
|
||
if (analysis.feeStructure.platform_fee && analysis.feeStructure.platform_fee > 50) {
|
||
recommendations.push({
|
||
type: 'platform_fee',
|
||
description: '建议升级商户等级,享受平台服务费折扣',
|
||
potentialSavings: analysis.feeStructure.platform_fee * 0.2, // 假设可节省20%
|
||
priority: 'high',
|
||
});
|
||
}
|
||
|
||
// 交易手续费优化
|
||
if (analysis.feeStructure.transaction_fee && analysis.feeStructure.transaction_fee > 30) {
|
||
recommendations.push({
|
||
type: 'transaction_fee',
|
||
description: '建议优化支付方式,降低交易手续费',
|
||
potentialSavings: analysis.feeStructure.transaction_fee * 0.15, // 假设可节省15%
|
||
priority: 'medium',
|
||
});
|
||
}
|
||
|
||
// 物流费用优化
|
||
if (analysis.feeStructure.logistics_fee && analysis.feeStructure.logistics_fee > 60) {
|
||
recommendations.push({
|
||
type: 'logistics_fee',
|
||
description: '建议与物流商协商折扣,优化物流方案',
|
||
potentialSavings: analysis.feeStructure.logistics_fee * 0.25, // 假设可节省25%
|
||
priority: 'high',
|
||
});
|
||
}
|
||
|
||
// 营销费用优化
|
||
if (analysis.feeStructure.marketing_fee && analysis.feeStructure.marketing_fee > 20) {
|
||
recommendations.push({
|
||
type: 'marketing_fee',
|
||
description: '建议优化营销策略,提高营销ROI',
|
||
potentialSavings: analysis.feeStructure.marketing_fee * 0.3, // 假设可节省30%
|
||
priority: 'medium',
|
||
});
|
||
}
|
||
|
||
// 订单量优化
|
||
if (analysis.orderCount < 50) {
|
||
recommendations.push({
|
||
type: 'order_volume',
|
||
description: '建议增加订单量,分摊固定成本',
|
||
potentialSavings: analysis.totalFees * 0.1, // 假设可节省10%
|
||
priority: 'low',
|
||
});
|
||
}
|
||
|
||
// 平均订单价值优化
|
||
if (analysis.averageOrderValue < 100) {
|
||
recommendations.push({
|
||
type: 'average_order_value',
|
||
description: '建议提高平均订单价值,增加利润率',
|
||
potentialSavings: analysis.totalFees * 0.05, // 假设可节省5%
|
||
priority: 'low',
|
||
});
|
||
}
|
||
|
||
return recommendations;
|
||
}
|
||
|
||
/**
|
||
* 计算优化后的金额
|
||
*/
|
||
private calculateOptimizedAmount(originalAmount: number, recommendations: OptimizationResult['recommendations']): number {
|
||
const totalSavings = recommendations.reduce((sum, rec) => sum + rec.potentialSavings, 0);
|
||
const optimizedAmount = originalAmount - totalSavings;
|
||
return Math.max(0, optimizedAmount);
|
||
}
|
||
|
||
/**
|
||
* 生成优化后的费用明细
|
||
*/
|
||
private generateOptimizedFees(originalFees: FeeData[], recommendations: OptimizationResult['recommendations']): FeeData[] {
|
||
const optimizedFees = [...originalFees];
|
||
|
||
// 应用优化建议到费用明细
|
||
recommendations.forEach(rec => {
|
||
const feeIndex = optimizedFees.findIndex(fee => fee.feeType === rec.type);
|
||
if (feeIndex !== -1) {
|
||
optimizedFees[feeIndex] = {
|
||
...optimizedFees[feeIndex],
|
||
amount: Math.max(0, optimizedFees[feeIndex].amount - rec.potentialSavings),
|
||
description: `${optimizedFees[feeIndex].description} (优化后)`,
|
||
};
|
||
}
|
||
});
|
||
|
||
return optimizedFees;
|
||
}
|
||
|
||
/**
|
||
* 计算置信度
|
||
*/
|
||
private calculateConfidence(recommendations: OptimizationResult['recommendations']): number {
|
||
// 基于建议数量和优先级计算置信度
|
||
const highPriorityCount = recommendations.filter(r => r.priority === 'high').length;
|
||
const mediumPriorityCount = recommendations.filter(r => r.priority === 'medium').length;
|
||
|
||
const confidence = 70 + (highPriorityCount * 5) + (mediumPriorityCount * 3);
|
||
return Math.min(95, confidence); // 最高95%
|
||
}
|
||
|
||
/**
|
||
* 获取商户结算优化报告
|
||
* @param merchantId 商户ID
|
||
* @param period 时间周期(如:'monthly', 'quarterly', 'yearly')
|
||
* @param traceId 链路追踪ID
|
||
* @returns 结算优化报告
|
||
*/
|
||
async getSettlementOptimizationReport(merchantId: string, period: string, traceId: string) {
|
||
this.logger.log(`获取商户结算优化报告: ${merchantId}, 周期: ${period}`, { traceId, merchantId, period });
|
||
|
||
try {
|
||
// 计算时间范围
|
||
const endDate = new Date();
|
||
let startDate = new Date();
|
||
|
||
switch (period) {
|
||
case 'monthly':
|
||
startDate.setMonth(startDate.getMonth() - 1);
|
||
break;
|
||
case 'quarterly':
|
||
startDate.setMonth(startDate.getMonth() - 3);
|
||
break;
|
||
case 'yearly':
|
||
startDate.setFullYear(startDate.getFullYear() - 1);
|
||
break;
|
||
default:
|
||
startDate.setMonth(startDate.getMonth() - 1);
|
||
}
|
||
|
||
// 获取时间范围内的结算单
|
||
const settlements = await this.prisma.settlement.findMany({
|
||
where: {
|
||
merchantId,
|
||
periodStart: {
|
||
gte: startDate,
|
||
},
|
||
periodEnd: {
|
||
lte: endDate,
|
||
},
|
||
},
|
||
});
|
||
|
||
// 批量优化结算单
|
||
const settlementIds = settlements.map(s => s.id);
|
||
const optimizationResults = await this.batchOptimizeSettlements(settlementIds, traceId);
|
||
|
||
// 计算总体优化效果
|
||
const totalOriginalAmount = optimizationResults.reduce((sum, result) => sum + result.originalAmount, 0);
|
||
const totalOptimizedAmount = optimizationResults.reduce((sum, result) => sum + result.optimizedAmount, 0);
|
||
const totalSavings = totalOriginalAmount - totalOptimizedAmount;
|
||
const totalOptimizationRate = totalOriginalAmount > 0 ? (totalSavings / totalOriginalAmount) * 100 : 0;
|
||
|
||
// 汇总建议
|
||
const allRecommendations = optimizationResults.flatMap(result => result.recommendations);
|
||
const recommendationCounts = allRecommendations.reduce((acc, rec) => {
|
||
acc[rec.type] = (acc[rec.type] || 0) + 1;
|
||
return acc;
|
||
}, {} as Record<string, number>);
|
||
|
||
const topRecommendations = Object.entries(recommendationCounts)
|
||
.sort(([,a], [,b]) => b - a)
|
||
.slice(0, 3)
|
||
.map(([type, count]) => {
|
||
const recommendation = allRecommendations.find(rec => rec.type === type);
|
||
return {
|
||
type,
|
||
count,
|
||
description: recommendation?.description || '',
|
||
potentialSavings: recommendation?.potentialSavings || 0,
|
||
};
|
||
});
|
||
|
||
const report = {
|
||
merchantId,
|
||
period,
|
||
timeRange: {
|
||
start: startDate,
|
||
end: endDate,
|
||
},
|
||
totalOriginalAmount,
|
||
totalOptimizedAmount,
|
||
totalSavings,
|
||
totalOptimizationRate: Math.round(totalOptimizationRate * 100) / 100,
|
||
settlementCount: settlements.length,
|
||
optimizationResults,
|
||
topRecommendations,
|
||
};
|
||
|
||
this.logger.log(`获取商户结算优化报告完成: ${merchantId}`, { traceId, report });
|
||
|
||
return report;
|
||
} catch (error) {
|
||
this.logger.error(`获取商户结算优化报告失败: ${error.message}`, { traceId, merchantId, period, error });
|
||
throw error;
|
||
}
|
||
}
|
||
}
|