Files
makemd/server/src/core/ai/SettlementOptimizationService.ts
wurenzhi b31591e04c feat: 实现多商户管理模块与前端服务
refactor: 优化服务层代码并修复类型问题

docs: 更新开发进度文档

feat(merchant): 新增商户监控与数据统计服务

feat(dashboard): 添加商户管理前端页面与服务

fix: 修复类型转换与可选参数处理

feat: 实现商户订单、店铺与结算管理功能

refactor: 重构审计日志格式与服务调用

feat: 新增商户入驻与身份注册功能

fix(controller): 修复路由参数类型问题

feat: 添加商户排名与统计报告功能

chore: 更新模拟数据与服务配置
2026-03-18 13:38:05 +08:00

436 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}