feat: 初始化项目结构并添加核心功能模块
- 新增文档模板和导航结构 - 实现服务器基础API路由和控制器 - 添加扩展插件配置和前端框架 - 引入多租户和权限管理模块 - 集成日志和数据库配置 - 添加核心业务模型和类型定义
This commit is contained in:
613
server/src/core/ai/RecommendationService.ts
Normal file
613
server/src/core/ai/RecommendationService.ts
Normal file
@@ -0,0 +1,613 @@
|
||||
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<void> {
|
||||
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) {
|
||||
logger.error(`[Recommendation] Failed to record user behavior: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为权重
|
||||
*/
|
||||
private static getBehaviorWeight(behaviorType: string): number {
|
||||
const weights = {
|
||||
'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<void> {
|
||||
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) {
|
||||
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) {
|
||||
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<any> {
|
||||
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<any[]> {
|
||||
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<string> {
|
||||
// 新用户使用内容推荐
|
||||
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<any> {
|
||||
const preferences = {
|
||||
categories: new Map<string, number>(),
|
||||
tags: new Map<string, number>(),
|
||||
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 => {
|
||||
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 => {
|
||||
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<string[]> {
|
||||
// 简化实现:返回最近活跃的用户
|
||||
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: item.item_id,
|
||||
score: parseInt(item.interaction_count) / similarUsers.length
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门商品
|
||||
*/
|
||||
private static async getPopularItems(tenantId: string, count: number): Promise<string[]> {
|
||||
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<void> {
|
||||
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<any> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user