import { logger } from '../../utils/logger'; import { FeatureGovernanceService } from '../governance/FeatureGovernanceService'; import db from '../../config/database'; /** * [AI_REC_01] 推荐系统服务 * @description 基于用户行为的智能推荐,支持协同过滤、内容推荐、混合推荐 */ export class RecommendationService { private static readonly USER_BEHAVIOR_TABLE = 'cf_ai_rec_user_behavior'; private static readonly ITEM_ATTRIBUTES_TABLE = 'cf_ai_rec_item_attributes'; private static readonly RECOMMENDATION_CACHE_TABLE = 'cf_ai_rec_cache'; private static readonly ALGORITHM_CONFIG_TABLE = 'cf_ai_rec_algorithm_config'; /** * 初始化表结构 */ static async initTable() { // 创建用户行为记录表 const hasBehaviorTable = await db.schema.hasTable(this.USER_BEHAVIOR_TABLE); if (!hasBehaviorTable) { console.log(`📦 Creating ${this.USER_BEHAVIOR_TABLE} table...`); await db.schema.createTable(this.USER_BEHAVIOR_TABLE, (table) => { table.increments('id').primary(); table.string('tenant_id', 64).notNullable(); table.string('user_id', 64).notNullable(); table.string('item_id', 64).notNullable(); table.enum('behavior_type', ['view', 'click', 'purchase', 'favorite']).notNullable(); table.decimal('weight', 5, 2).defaultTo(1.0); // 行为权重 table.timestamp('timestamp').defaultTo(db.fn.now()); table.json('context'); // 上下文信息 table.index(['tenant_id', 'user_id']); table.index(['tenant_id', 'item_id']); table.index(['tenant_id', 'timestamp']); }); } // 创建商品属性表 const hasItemTable = await db.schema.hasTable(this.ITEM_ATTRIBUTES_TABLE); if (!hasItemTable) { console.log(`📦 Creating ${this.ITEM_ATTRIBUTES_TABLE} table...`); await db.schema.createTable(this.ITEM_ATTRIBUTES_TABLE, (table) => { table.string('tenant_id', 64).notNullable(); table.string('item_id', 64).notNullable(); table.string('category', 100); table.decimal('price', 10, 2); table.json('tags'); // 商品标签 table.integer('popularity').defaultTo(0); // 热度 table.timestamp('created_at').defaultTo(db.fn.now()); table.primary(['tenant_id', 'item_id']); table.index(['tenant_id', 'category']); table.index(['tenant_id', 'popularity']); }); } // 创建推荐缓存表 const hasCacheTable = await db.schema.hasTable(this.RECOMMENDATION_CACHE_TABLE); if (!hasCacheTable) { console.log(`📦 Creating ${this.RECOMMENDATION_CACHE_TABLE} table...`); await db.schema.createTable(this.RECOMMENDATION_CACHE_TABLE, (table) => { table.increments('id').primary(); table.string('tenant_id', 64).notNullable(); table.string('user_id', 64).notNullable(); table.json('recommendations').notNullable(); // 推荐商品ID列表 table.string('algorithm', 50).notNullable(); // 使用的算法 table.decimal('score', 5, 2); // 推荐质量评分 table.timestamp('created_at').defaultTo(db.fn.now()); table.timestamp('expires_at').notNullable(); table.index(['tenant_id', 'user_id']); table.index(['tenant_id', 'algorithm']); table.index(['tenant_id', 'expires_at']); }); } // 创建算法配置表 const hasConfigTable = await db.schema.hasTable(this.ALGORITHM_CONFIG_TABLE); if (!hasConfigTable) { console.log(`📦 Creating ${this.ALGORITHM_CONFIG_TABLE} table...`); await db.schema.createTable(this.ALGORITHM_CONFIG_TABLE, (table) => { table.increments('id').primary(); table.string('tenant_id', 64).notNullable(); table.string('algorithm_name', 50).notNullable(); table.json('parameters').notNullable(); // 算法参数 table.boolean('enabled').defaultTo(true); table.decimal('weight', 5, 2).defaultTo(1.0); // 算法权重 table.timestamp('updated_at').defaultTo(db.fn.now()); table.unique(['tenant_id', 'algorithm_name']); }); } console.log('✅ RecommendationService tables initialized'); } /** * 记录用户行为 */ static async recordUserBehavior(params: { tenantId: string; userId: string; itemId: string; behaviorType: 'view' | 'click' | 'purchase' | 'favorite'; weight?: number; context?: any; }): Promise { try { await db(this.USER_BEHAVIOR_TABLE).insert({ tenant_id: params.tenantId, user_id: params.userId, item_id: params.itemId, behavior_type: params.behaviorType, weight: params.weight || this.getBehaviorWeight(params.behaviorType), context: params.context || {}, timestamp: new Date() }); logger.info(`[Recommendation] User behavior recorded: ${params.userId} -> ${params.itemId} (${params.behaviorType})`); } catch (error: any) { logger.error(`[Recommendation] Failed to record user behavior: ${error.message}`); throw error; } } /** * 获取行为权重 */ private static getBehaviorWeight(behaviorType: string): number { const weights: { [key: string]: number } = { 'view': 1.0, 'click': 2.0, 'favorite': 3.0, 'purchase': 5.0 }; return weights[behaviorType] || 1.0; } /** * 更新商品属性 */ static async updateItemAttributes(params: { tenantId: string; itemId: string; category?: string; price?: number; tags?: string[]; popularity?: number; }): Promise { try { const existing = await db(this.ITEM_ATTRIBUTES_TABLE) .where({ tenant_id: params.tenantId, item_id: params.itemId }) .first(); if (existing) { await db(this.ITEM_ATTRIBUTES_TABLE) .where({ tenant_id: params.tenantId, item_id: params.itemId }) .update({ category: params.category || existing.category, price: params.price || existing.price, tags: params.tags || existing.tags, popularity: params.popularity || existing.popularity }); } else { await db(this.ITEM_ATTRIBUTES_TABLE).insert({ tenant_id: params.tenantId, item_id: params.itemId, category: params.category || '', price: params.price || 0, tags: params.tags || [], popularity: params.popularity || 0 }); } logger.info(`[Recommendation] Item attributes updated: ${params.itemId}`); } catch (error: any) { logger.error(`[Recommendation] Failed to update item attributes: ${error.message}`); throw error; } } /** * 获取推荐结果 */ static async getRecommendations(params: { tenantId: string; userId: string; count?: number; algorithms?: string[]; context?: any; }): Promise<{ recommendations: string[]; algorithm: string; score: number; fromCache: boolean; }> { const startTime = Date.now(); const count = params.count || 10; try { // 1. 检查缓存 const cached = await this.getCachedRecommendations(params.tenantId, params.userId); if (cached && cached.recommendations.length >= count) { logger.info(`[Recommendation] Using cached recommendations for user: ${params.userId}`); return { recommendations: cached.recommendations.slice(0, count), algorithm: cached.algorithm, score: cached.score, fromCache: true }; } // 2. 获取用户行为历史 const userHistory = await this.getUserBehaviorHistory(params.tenantId, params.userId, 100); // 3. 选择算法并计算推荐 const algorithm = await this.selectAlgorithm(params.tenantId, userHistory, params.context); const recommendations = await this.calculateRecommendations( params.tenantId, params.userId, userHistory, algorithm, count ); // 4. 缓存结果 await this.cacheRecommendations(params.tenantId, params.userId, recommendations, algorithm); const processingTime = Date.now() - startTime; logger.info(`[Recommendation] Recommendations generated for user: ${params.userId}, algorithm: ${algorithm}, time: ${processingTime}ms`); return { recommendations: recommendations.map(r => r.itemId), algorithm, score: this.calculateRecommendationScore(recommendations), fromCache: false }; } catch (error: any) { logger.error(`[Recommendation] Failed to get recommendations: ${error.message}`); // 降级策略:返回热门商品 const fallback = await this.getPopularItems(params.tenantId, count); return { recommendations: fallback, algorithm: 'fallback_popular', score: 0.5, fromCache: false }; } } /** * 获取缓存的推荐结果 */ private static async getCachedRecommendations(tenantId: string, userId: string): Promise { const cache = await db(this.RECOMMENDATION_CACHE_TABLE) .where({ tenant_id: tenantId, user_id: userId }) .where('expires_at', '>', new Date()) .orderBy('created_at', 'desc') .first(); return cache; } /** * 获取用户行为历史 */ private static async getUserBehaviorHistory(tenantId: string, userId: string, limit: number): Promise { return await db(this.USER_BEHAVIOR_TABLE) .where({ tenant_id: tenantId, user_id: userId }) .orderBy('timestamp', 'desc') .limit(limit); } /** * 选择推荐算法 */ private static async selectAlgorithm(tenantId: string, userHistory: any[], context?: any): Promise { // 新用户使用内容推荐 if (userHistory.length < 5) { return 'content_based'; } // 活跃用户使用协同过滤 if (userHistory.length >= 20) { return 'collaborative_filtering'; } // 默认使用混合推荐 return 'hybrid'; } /** * 计算推荐结果 */ private static async calculateRecommendations( tenantId: string, userId: string, userHistory: any[], algorithm: string, count: number ): Promise<{itemId: string, score: number}[]> { switch (algorithm) { case 'content_based': return await this.contentBasedRecommendation(tenantId, userHistory, count); case 'collaborative_filtering': return await this.collaborativeFiltering(tenantId, userId, userHistory, count); case 'hybrid': return await this.hybridRecommendation(tenantId, userId, userHistory, count); default: return await this.getPopularItemsWithScore(tenantId, count); } } /** * 内容推荐算法 */ private static async contentBasedRecommendation(tenantId: string, userHistory: any[], count: number): Promise<{itemId: string, score: number}[]> { // 获取用户偏好的商品类别和标签 const userPreferences = await this.analyzeUserPreferences(tenantId, userHistory); // 基于偏好推荐相似商品 const allItems = await db(this.ITEM_ATTRIBUTES_TABLE) .where({ tenant_id: tenantId }) .select('item_id', 'category', 'tags', 'popularity'); const scoredItems = allItems.map(item => ({ itemId: item.item_id, score: this.calculateContentSimilarity(userPreferences, item) })); return scoredItems .sort((a, b) => b.score - a.score) .slice(0, count); } /** * 协同过滤算法 */ private static async collaborativeFiltering(tenantId: string, userId: string, userHistory: any[], count: number): Promise<{itemId: string, score: number}[]> { // 基于用户-商品交互矩阵的推荐 const similarUsers = await this.findSimilarUsers(tenantId, userId, userHistory); const recommendedItems = await this.getItemsFromSimilarUsers(tenantId, similarUsers, userHistory); return recommendedItems.slice(0, count); } /** * 混合推荐算法 */ private static async hybridRecommendation(tenantId: string, userId: string, userHistory: any[], count: number): Promise<{itemId: string, score: number}[]> { // 并行计算多种算法的推荐结果 const [contentRecs, cfRecs] = await Promise.all([ this.contentBasedRecommendation(tenantId, userHistory, count * 2), this.collaborativeFiltering(tenantId, userId, userHistory, count * 2) ]); // 合并并去重 const merged = [...contentRecs, ...cfRecs]; const uniqueItems = this.removeDuplicates(merged); // 重新评分和排序 return uniqueItems .sort((a, b) => b.score - a.score) .slice(0, count); } /** * 分析用户偏好 */ private static async analyzeUserPreferences(tenantId: string, userHistory: any[]): Promise { const preferences = { categories: new Map(), tags: new Map(), priceRange: { min: Infinity, max: 0 } }; for (const behavior of userHistory) { const item = await db(this.ITEM_ATTRIBUTES_TABLE) .where({ tenant_id: tenantId, item_id: behavior.item_id }) .first(); if (item) { // 统计类别偏好 if (item.category) { const current = preferences.categories.get(item.category) || 0; preferences.categories.set(item.category, current + behavior.weight); } // 统计标签偏好 if (item.tags && Array.isArray(item.tags)) { item.tags.forEach((tag: string) => { const current = preferences.tags.get(tag) || 0; preferences.tags.set(tag, current + behavior.weight); }); } // 统计价格偏好 if (item.price) { preferences.priceRange.min = Math.min(preferences.priceRange.min, item.price); preferences.priceRange.max = Math.max(preferences.priceRange.max, item.price); } } } return preferences; } /** * 计算内容相似度 */ private static calculateContentSimilarity(userPreferences: any, item: any): number { let score = 0; // 类别匹配 if (item.category && userPreferences.categories.has(item.category)) { score += userPreferences.categories.get(item.category) * 0.4; } // 标签匹配 if (item.tags && Array.isArray(item.tags)) { item.tags.forEach((tag: string) => { if (userPreferences.tags.has(tag)) { score += userPreferences.tags.get(tag) * 0.3; } }); } // 价格偏好匹配 if (item.price && userPreferences.priceRange.min !== Infinity) { const priceScore = this.calculatePriceSimilarity(item.price, userPreferences.priceRange); score += priceScore * 0.3; } // 热度加成 score += (item.popularity || 0) * 0.1; return score; } /** * 计算价格相似度 */ private static calculatePriceSimilarity(price: number, priceRange: any): number { const range = priceRange.max - priceRange.min; if (range === 0) return 1.0; const normalized = (price - priceRange.min) / range; return 1.0 - Math.abs(normalized - 0.5) * 2; // 偏好中间价格 } /** * 查找相似用户 */ private static async findSimilarUsers(tenantId: string, userId: string, userHistory: any[]): Promise { // 简化实现:返回最近活跃的用户 const recentUsers = await db(this.USER_BEHAVIOR_TABLE) .where({ tenant_id: tenantId }) .where('user_id', '!=', userId) .select('user_id') .distinct() .orderBy('timestamp', 'desc') .limit(100); return recentUsers.map(u => u.user_id); } /** * 从相似用户获取商品 */ private static async getItemsFromSimilarUsers(tenantId: string, similarUsers: string[], userHistory: any[]): Promise<{itemId: string, score: number}[]> { const userItemIds = new Set(userHistory.map(h => h.item_id)); const itemsFromSimilarUsers = await db(this.USER_BEHAVIOR_TABLE) .where({ tenant_id: tenantId }) .whereIn('user_id', similarUsers) .whereNotIn('item_id', Array.from(userItemIds)) .select('item_id') .groupBy('item_id') .count('* as interaction_count') .orderBy('interaction_count', 'desc') .limit(50); return itemsFromSimilarUsers.map(item => ({ itemId: String(item.item_id), score: parseInt(String(item.interaction_count)) / similarUsers.length })); } /** * 获取热门商品 */ private static async getPopularItems(tenantId: string, count: number): Promise { const items = await db(this.ITEM_ATTRIBUTES_TABLE) .where({ tenant_id: tenantId }) .orderBy('popularity', 'desc') .limit(count) .select('item_id'); return items.map(item => item.item_id); } /** * 获取带分数的热门商品 */ private static async getPopularItemsWithScore(tenantId: string, count: number): Promise<{itemId: string, score: number}[]> { const items = await db(this.ITEM_ATTRIBUTES_TABLE) .where({ tenant_id: tenantId }) .orderBy('popularity', 'desc') .limit(count) .select('item_id', 'popularity'); const maxPopularity = Math.max(...items.map(item => item.popularity || 0)); return items.map(item => ({ itemId: item.item_id, score: maxPopularity > 0 ? (item.popularity || 0) / maxPopularity : 0.5 })); } /** * 缓存推荐结果 */ private static async cacheRecommendations(tenantId: string, userId: string, recommendations: {itemId: string, score: number}[], algorithm: string): Promise { const expiresAt = new Date(Date.now() + 30 * 60 * 1000); // 30分钟缓存 await db(this.RECOMMENDATION_CACHE_TABLE).insert({ tenant_id: tenantId, user_id: userId, recommendations: recommendations.map(r => r.itemId), algorithm, score: this.calculateRecommendationScore(recommendations), expires_at: expiresAt }); } /** * 计算推荐质量评分 */ private static calculateRecommendationScore(recommendations: {itemId: string, score: number}[]): number { if (recommendations.length === 0) return 0; const avgScore = recommendations.reduce((sum, r) => sum + r.score, 0) / recommendations.length; const diversity = this.calculateDiversity(recommendations); return avgScore * 0.7 + diversity * 0.3; } /** * 计算推荐多样性 */ private static calculateDiversity(recommendations: {itemId: string, score: number}[]): number { if (recommendations.length <= 1) return 1.0; // 简化实现:基于分数分布的多样性 const scores = recommendations.map(r => r.score); const variance = this.calculateVariance(scores); return Math.min(variance * 10, 1.0); } /** * 计算方差 */ private static calculateVariance(numbers: number[]): number { const mean = numbers.reduce((sum, num) => sum + num, 0) / numbers.length; const squaredDiffs = numbers.map(num => Math.pow(num - mean, 2)); return squaredDiffs.reduce((sum, diff) => sum + diff, 0) / numbers.length; } /** * 移除重复项 */ private static removeDuplicates(items: {itemId: string, score: number}[]): {itemId: string, score: number}[] { const seen = new Set(); return items.filter(item => { if (seen.has(item.itemId)) { return false; } seen.add(item.itemId); return true; }); } /** * 获取推荐效果统计 */ static async getRecommendationStats(tenantId: string): Promise { const stats = await db(this.RECOMMENDATION_CACHE_TABLE) .where({ tenant_id: tenantId }) .select( db.raw('COUNT(*) as total_recommendations'), db.raw('AVG(score) as avg_score'), db.raw('MAX(score) as max_score'), db.raw('MIN(score) as min_score'), db.raw('COUNT(DISTINCT user_id) as unique_users') ) .first(); return { totalRecommendations: stats.total_recommendations || 0, avgScore: parseFloat(stats.avg_score || 0).toFixed(3), maxScore: parseFloat(stats.max_score || 0).toFixed(3), minScore: parseFloat(stats.min_score || 0).toFixed(3), uniqueUsers: stats.unique_users || 0 }; } /** * 清理过期缓存 */ static async cleanupExpiredCache(tenantId: string): Promise { await db(this.RECOMMENDATION_CACHE_TABLE) .where({ tenant_id: tenantId }) .where('expires_at', '<', new Date()) .delete(); logger.info(`[Recommendation] Expired cache cleaned for tenant: ${tenantId}`); } }