feat: 初始化项目结构并添加核心功能模块

- 新增文档模板和导航结构
- 实现服务器基础API路由和控制器
- 添加扩展插件配置和前端框架
- 引入多租户和权限管理模块
- 集成日志和数据库配置
- 添加核心业务模型和类型定义
This commit is contained in:
2026-03-17 22:07:19 +08:00
parent c0870dce50
commit 136c2fa579
728 changed files with 107690 additions and 5614 deletions

View 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}`);
}
}