97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
|
|
import db from '../config/database';
|
||
|
|
import { logger } from '../utils/logger';
|
||
|
|
import { AuditService } from './AuditService';
|
||
|
|
|
||
|
|
export interface AbandonedCartInfo {
|
||
|
|
userId: string;
|
||
|
|
cartId: string;
|
||
|
|
productId: string;
|
||
|
|
abandonedAt: Date;
|
||
|
|
recoveryEmailSent?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* MarketingService: 自动营销挽留系统 (BIZ_EXT_12)
|
||
|
|
* 监听流失行为并自动执行阶梯优惠挽留策略
|
||
|
|
*/
|
||
|
|
export class MarketingService {
|
||
|
|
private static TABLE_CART = 'cf_cart';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 扫描 24 小时前未下单的加购记录并触发挽留
|
||
|
|
*/
|
||
|
|
static async processAbandonedCarts(): Promise<void> {
|
||
|
|
try {
|
||
|
|
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||
|
|
const dayBeforeYesterday = new Date(Date.now() - 48 * 60 * 60 * 1000);
|
||
|
|
|
||
|
|
// 获取 24-48 小时内未结账且未发送挽留邮件的记录
|
||
|
|
const abandonedCarts = await db(this.TABLE_CART)
|
||
|
|
.where({ status: 'ABANDONED', recoverySent: false })
|
||
|
|
.whereBetween('updated_at', [dayBeforeYesterday, yesterday])
|
||
|
|
.select('*');
|
||
|
|
|
||
|
|
for (const cart of abandonedCarts) {
|
||
|
|
await this.triggerRecoveryStrategy(cart);
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error(`[AbandonedRecovery] Failed to process carts: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 执行阶梯优惠挽留策略
|
||
|
|
*/
|
||
|
|
private static async triggerRecoveryStrategy(cart: any): Promise<void> {
|
||
|
|
try {
|
||
|
|
// 1. 获取用户历史价值 (LTV) 评分
|
||
|
|
const customerScore = await this.getCustomerScore(cart.userId);
|
||
|
|
|
||
|
|
let discountCode: string;
|
||
|
|
let emailContent: string;
|
||
|
|
|
||
|
|
// 2. 阶梯策略:根据用户价值决定优惠力度
|
||
|
|
if (customerScore > 80) {
|
||
|
|
discountCode = 'VIP_RECOVER_20'; // 8折
|
||
|
|
emailContent = `[VIP Exclusive] Still thinking about it? Here's 20% off just for you!`;
|
||
|
|
} else if (customerScore > 50) {
|
||
|
|
discountCode = 'SAVE_10_NOW'; // 9折
|
||
|
|
emailContent = `We noticed you left something behind. Enjoy 10% off if you order now!`;
|
||
|
|
} else {
|
||
|
|
discountCode = 'COME_BACK_5'; // 95折
|
||
|
|
emailContent = `Complete your purchase and get 5% off!`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 记录审计并标记已发送 (Mock 邮件发送逻辑)
|
||
|
|
logger.info(`[AbandonedRecovery] Sending ${discountCode} to user ${cart.userId} for cart ${cart.id}`);
|
||
|
|
|
||
|
|
await db(this.TABLE_CART).where({ id: cart.id }).update({
|
||
|
|
recoverySent: true,
|
||
|
|
recoveryStrategy: discountCode,
|
||
|
|
recovery_at: new Date()
|
||
|
|
});
|
||
|
|
|
||
|
|
// 发送审计日志 (BIZ_DEV_04)
|
||
|
|
await AuditService.log({
|
||
|
|
module: 'MARKETING',
|
||
|
|
action: 'RECOVERY_SENT',
|
||
|
|
userId: cart.userId,
|
||
|
|
tenantId: 'SYSTEM', // 实际应从用户上下文获取
|
||
|
|
traceId: `RECOVER-${Date.now()}`,
|
||
|
|
resourceType: 'cart',
|
||
|
|
resourceId: String(cart.id),
|
||
|
|
afterSnapshot: { discountCode, emailContent },
|
||
|
|
source: 'node',
|
||
|
|
result: 'success'
|
||
|
|
});
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error(`[AbandonedRecovery] Strategy failed for user ${cart.userId}: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static async getCustomerScore(userId: string): Promise<number> {
|
||
|
|
// 模拟根据历史 LTV 获取的用户得分 (0-100)
|
||
|
|
return Math.floor(Math.random() * 100);
|
||
|
|
}
|
||
|
|
}
|