refactor: 重构DisputeResolverService和DIDHandshakeService fix: 修复SovereignWealthFundService中的表名错误 docs: 更新AI模块清单和任务总览文档 chore: 添加多个README文件说明项目结构 style: 优化logger日志输出格式 perf: 改进RecommendationService的性能和类型安全 test: 添加DomainBootstrap和test-domain-bootstrap测试文件 build: 配置dashboard的umi相关文件 ci: 添加GitHub工作流配置
612 lines
20 KiB
TypeScript
612 lines
20 KiB
TypeScript
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: 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<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: 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<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: 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<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: String(item.item_id),
|
|
score: parseInt(String(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}`);
|
|
}
|
|
} |