Skip to Content

博客系统

本文介绍如何使用 Entity Engine 构建一个功能完整的博客系统,涵盖文章发布、评论管理、用户互动、SEO优化和内容推荐等核心功能。

系统架构

核心实体模型

// 博客模型 export const BlogModel: IEntityModel = { name: 'Blog', fields: { id: { type: 'uuid', primaryKey: true }, name: { type: 'string', required: true }, slug: { type: 'string', unique: true, required: true }, description: { type: 'text' }, tagline: { type: 'string' }, logo: { type: 'string' }, favicon: { type: 'string' }, // 配置信息 settings: { type: 'json', defaultValue: {} }, theme: { type: 'string', defaultValue: 'default' }, language: { type: 'string', defaultValue: 'en' }, timezone: { type: 'string', defaultValue: 'UTC' }, // SEO设置 seoTitle: { type: 'string' }, seoDescription: { type: 'string' }, seoKeywords: { type: 'string' }, // 社交媒体 socialLinks: { type: 'json', defaultValue: {} }, // 关系 posts: { type: 'relation', relation: { type: 'one_to_many', model: 'Post', foreignKey: 'blogId' } }, categories: { type: 'relation', relation: { type: 'one_to_many', model: 'Category', foreignKey: 'blogId' } }, authors: { type: 'relation', relation: { type: 'many_to_many', model: 'User', through: 'BlogAuthor' } }, // 统计信息 postCount: { type: 'number', defaultValue: 0 }, subscriberCount: { type: 'number', defaultValue: 0 }, totalViews: { type: 'number', defaultValue: 0 }, // 状态 isActive: { type: 'boolean', defaultValue: true }, isPublic: { type: 'boolean', defaultValue: true }, // 时间戳 createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['slug'], unique: true }, { fields: ['isActive'] }, { fields: ['isPublic'] } ] }; // 文章模型 export const PostModel: IEntityModel = { name: 'Post', fields: { id: { type: 'uuid', primaryKey: true }, blogId: { type: 'uuid', required: true }, blog: { type: 'relation', relation: { type: 'many_to_one', model: 'Blog', targetKey: 'id' } }, // 基本信息 title: { type: 'string', required: true }, slug: { type: 'string', required: true }, excerpt: { type: 'text' }, content: { type: 'text', required: true }, plainTextContent: { type: 'text' }, // 用于搜索和摘要生成 // 媒体资源 featuredImage: { type: 'string' }, featuredImageAlt: { type: 'string' }, gallery: { type: 'json', defaultValue: [] }, // 分类和标签 categoryId: { type: 'uuid' }, category: { type: 'relation', relation: { type: 'many_to_one', model: 'Category', targetKey: 'id' } }, tags: { type: 'relation', relation: { type: 'many_to_many', model: 'Tag', through: 'PostTag' } }, // 作者信息 authorId: { type: 'uuid', required: true }, author: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, coAuthors: { type: 'relation', relation: { type: 'many_to_many', model: 'User', through: 'PostCoAuthor' } }, // 状态管理 status: { type: 'enum', enumValues: ['DRAFT', 'PENDING', 'PUBLISHED', 'ARCHIVED'], defaultValue: 'DRAFT' }, visibility: { type: 'enum', enumValues: ['PUBLIC', 'PRIVATE', 'PASSWORD', 'UNLISTED'], defaultValue: 'PUBLIC' }, password: { type: 'string' }, // 发布设置 publishedAt: { type: 'datetime' }, scheduledFor: { type: 'datetime' }, // SEO设置 seoTitle: { type: 'string' }, seoDescription: { type: 'string' }, seoKeywords: { type: 'string' }, canonicalUrl: { type: 'string' }, // 社交媒体 socialTitle: { type: 'string' }, socialDescription: { type: 'string' }, socialImage: { type: 'string' }, // 内容设置 allowComments: { type: 'boolean', defaultValue: true }, isPinned: { type: 'boolean', defaultValue: false }, isFeatured: { type: 'boolean', defaultValue: false }, format: { type: 'enum', enumValues: ['STANDARD', 'GALLERY', 'VIDEO', 'AUDIO', 'QUOTE', 'LINK'], defaultValue: 'STANDARD' }, // 阅读设置 readingTime: { type: 'number' }, // 预估阅读时间(分钟) wordCount: { type: 'number' }, // 统计数据 viewCount: { type: 'number', defaultValue: 0 }, likeCount: { type: 'number', defaultValue: 0 }, shareCount: { type: 'number', defaultValue: 0 }, commentCount: { type: 'number', defaultValue: 0 }, // 关系 comments: { type: 'relation', relation: { type: 'one_to_many', model: 'Comment', foreignKey: 'postId' } }, series: { type: 'relation', relation: { type: 'many_to_many', model: 'Series', through: 'SeriesPost' } }, // 版本控制 revisions: { type: 'relation', relation: { type: 'one_to_many', model: 'PostRevision', foreignKey: 'postId' } }, // 自定义字段 customFields: { type: 'json', defaultValue: {} }, metadata: { type: 'json', defaultValue: {} }, // 时间戳 createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['blogId', 'slug'], unique: true }, { fields: ['status'] }, { fields: ['publishedAt'] }, { fields: ['authorId'] }, { fields: ['categoryId'] }, { fields: ['isPinned'] }, { fields: ['isFeatured'] }, { fields: ['viewCount'] }, { fields: ['createdAt'] } ] }; // 分类模型 export const CategoryModel: IEntityModel = { name: 'Category', fields: { id: { type: 'uuid', primaryKey: true }, blogId: { type: 'uuid', required: true }, blog: { type: 'relation', relation: { type: 'many_to_one', model: 'Blog', targetKey: 'id' } }, name: { type: 'string', required: true }, slug: { type: 'string', required: true }, description: { type: 'text' }, color: { type: 'string' }, icon: { type: 'string' }, image: { type: 'string' }, // 层级结构 parentId: { type: 'uuid' }, parent: { type: 'relation', relation: { type: 'many_to_one', model: 'Category', targetKey: 'id' } }, children: { type: 'relation', relation: { type: 'one_to_many', model: 'Category', foreignKey: 'parentId' } }, // 排序和显示 sortOrder: { type: 'number', defaultValue: 0 }, isVisible: { type: 'boolean', defaultValue: true }, showInNavigation: { type: 'boolean', defaultValue: true }, // 关系 posts: { type: 'relation', relation: { type: 'one_to_many', model: 'Post', foreignKey: 'categoryId' } }, // SEO设置 seoTitle: { type: 'string' }, seoDescription: { type: 'string' }, seoKeywords: { type: 'string' }, // 统计 postCount: { type: 'number', defaultValue: 0 }, // 时间戳 createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['blogId', 'slug'], unique: true }, { fields: ['parentId'] }, { fields: ['isVisible', 'sortOrder'] } ] }; // 标签模型 export const TagModel: IEntityModel = { name: 'Tag', fields: { id: { type: 'uuid', primaryKey: true }, name: { type: 'string', required: true }, slug: { type: 'string', required: true }, description: { type: 'string' }, color: { type: 'string' }, // 关系 posts: { type: 'relation', relation: { type: 'many_to_many', model: 'Post', through: 'PostTag' } }, // SEO设置 seoTitle: { type: 'string' }, seoDescription: { type: 'string' }, // 统计 postCount: { type: 'number', defaultValue: 0 }, // 时间戳 createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['name'], unique: true }, { fields: ['slug'], unique: true }, { fields: ['postCount'] } ] }; // 评论模型 export const CommentModel: IEntityModel = { name: 'Comment', fields: { id: { type: 'uuid', primaryKey: true }, postId: { type: 'uuid', required: true }, post: { type: 'relation', relation: { type: 'many_to_one', model: 'Post', targetKey: 'id' } }, // 评论内容 content: { type: 'text', required: true }, contentHtml: { type: 'text' }, // 渲染后的HTML // 作者信息 authorName: { type: 'string', required: true }, authorEmail: { type: 'string', required: true }, authorUrl: { type: 'string' }, authorAvatar: { type: 'string' }, // 用户关联 userId: { type: 'uuid' }, user: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, // 层级结构(回复) parentId: { type: 'uuid' }, parent: { type: 'relation', relation: { type: 'many_to_one', model: 'Comment', targetKey: 'id' } }, replies: { type: 'relation', relation: { type: 'one_to_many', model: 'Comment', foreignKey: 'parentId' } }, // 状态管理 status: { type: 'enum', enumValues: ['PENDING', 'APPROVED', 'REJECTED', 'SPAM'], defaultValue: 'PENDING' }, moderationReason: { type: 'string' }, // 技术信息 ipAddress: { type: 'string' }, userAgent: { type: 'string' }, // 互动数据 likeCount: { type: 'number', defaultValue: 0 }, dislikeCount: { type: 'number', defaultValue: 0 }, replyCount: { type: 'number', defaultValue: 0 }, // 标记 isAuthorReply: { type: 'boolean', defaultValue: false }, isPinned: { type: 'boolean', defaultValue: false }, isEdited: { type: 'boolean', defaultValue: false }, editedAt: { type: 'datetime' }, // 时间戳 createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['postId'] }, { fields: ['parentId'] }, { fields: ['status'] }, { fields: ['userId'] }, { fields: ['createdAt'] } ] }; // 系列模型 export const SeriesModel: IEntityModel = { name: 'Series', fields: { id: { type: 'uuid', primaryKey: true }, blogId: { type: 'uuid', required: true }, blog: { type: 'relation', relation: { type: 'many_to_one', model: 'Blog', targetKey: 'id' } }, title: { type: 'string', required: true }, slug: { type: 'string', required: true }, description: { type: 'text' }, image: { type: 'string' }, // 作者 authorId: { type: 'uuid', required: true }, author: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, // 状态 status: { type: 'enum', enumValues: ['DRAFT', 'PUBLISHED', 'COMPLETED'], defaultValue: 'DRAFT' }, // 关系 posts: { type: 'relation', relation: { type: 'many_to_many', model: 'Post', through: 'SeriesPost' } }, // SEO设置 seoTitle: { type: 'string' }, seoDescription: { type: 'string' }, // 统计 postCount: { type: 'number', defaultValue: 0 }, totalViews: { type: 'number', defaultValue: 0 }, // 时间戳 createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['blogId', 'slug'], unique: true }, { fields: ['authorId'] }, { fields: ['status'] } ] }; // 订阅者模型 export const SubscriberModel: IEntityModel = { name: 'Subscriber', fields: { id: { type: 'uuid', primaryKey: true }, blogId: { type: 'uuid', required: true }, blog: { type: 'relation', relation: { type: 'many_to_one', model: 'Blog', targetKey: 'id' } }, email: { type: 'string', required: true }, firstName: { type: 'string' }, lastName: { type: 'string' }, // 订阅设置 subscriptionType: { type: 'enum', enumValues: ['ALL_POSTS', 'WEEKLY_DIGEST', 'MONTHLY_DIGEST'], defaultValue: 'ALL_POSTS' }, categories: { type: 'json', defaultValue: [] }, // 订阅的分类ID数组 tags: { type: 'json', defaultValue: [] }, // 订阅的标签数组 // 状态 status: { type: 'enum', enumValues: ['ACTIVE', 'UNSUBSCRIBED', 'BOUNCED'], defaultValue: 'ACTIVE' }, isConfirmed: { type: 'boolean', defaultValue: false }, confirmationToken: { type: 'string' }, confirmedAt: { type: 'datetime' }, // 统计 emailsSent: { type: 'number', defaultValue: 0 }, emailsOpened: { type: 'number', defaultValue: 0 }, linksClicked: { type: 'number', defaultValue: 0 }, lastEmailSentAt: { type: 'datetime' }, lastActivityAt: { type: 'datetime' }, // 取消订阅 unsubscribedAt: { type: 'datetime' }, unsubscribeReason: { type: 'string' }, // 时间戳 createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['blogId', 'email'], unique: true }, { fields: ['status'] }, { fields: ['isConfirmed'] }, { fields: ['createdAt'] } ] }; // 内容分析模型 export const AnalyticsModel: IEntityModel = { name: 'Analytics', fields: { id: { type: 'uuid', primaryKey: true }, // 关联对象 blogId: { type: 'uuid', required: true }, postId: { type: 'uuid' }, // 事件类型 eventType: { type: 'enum', enumValues: ['PAGE_VIEW', 'POST_VIEW', 'COMMENT', 'LIKE', 'SHARE', 'SEARCH', 'SUBSCRIPTION'], required: true }, // 用户信息 userId: { type: 'uuid' }, sessionId: { type: 'string' }, // 技术信息 ipAddress: { type: 'string' }, userAgent: { type: 'string' }, referer: { type: 'string' }, // 地理信息 country: { type: 'string' }, region: { type: 'string' }, city: { type: 'string' }, // 设备信息 deviceType: { type: 'enum', enumValues: ['DESKTOP', 'TABLET', 'MOBILE'], required: true }, browser: { type: 'string' }, os: { type: 'string' }, // 事件数据 eventData: { type: 'json', defaultValue: {} }, // 时间信息 duration: { type: 'number' }, // 页面停留时间(秒) timestamp: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['blogId', 'eventType'] }, { fields: ['postId', 'eventType'] }, { fields: ['timestamp'] }, { fields: ['userId'] } ] };

业务逻辑实现

博客内容管理动作处理器

// 博客内容管理动作处理器 class BlogContentActionHandler implements IActionHandler { name = 'blog-content'; displayName = '博客内容管理'; category = ActionCategory.BUSINESS; inputSchema: ActionInputSchema = { type: 'object', properties: { action: { type: 'string', enum: ['create_post', 'update_post', 'publish_post', 'schedule_post', 'delete_post'], required: true }, postId: { type: 'string' }, blogId: { type: 'string' }, data: { type: 'object' }, options: { type: 'object' } } }; async execute(context: ActionExecutionContext): Promise<ActionResult> { const { input, entityManager, user } = context; try { switch (input.action) { case 'create_post': return await this.createPost(input, entityManager, user); case 'update_post': return await this.updatePost(input, entityManager, user); case 'publish_post': return await this.publishPost(input, entityManager, user); case 'schedule_post': return await this.schedulePost(input, entityManager, user); case 'delete_post': return await this.deletePost(input, entityManager, user); default: throw new Error(`Unknown blog action: ${input.action}`); } } catch (error) { return { success: false, error: { message: error.message, code: 'BLOG_CONTENT_ERROR' } }; } } private async createPost(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { // 验证博客存在 const blog = await entityManager.get('Blog', input.blogId); if (!blog) { throw new Error('Blog not found'); } // 生成唯一slug const slug = await this.generateUniqueSlug(input.data.title, input.blogId, entityManager); // 计算文章统计信息 const plainTextContent = this.stripHtml(input.data.content); const wordCount = this.countWords(plainTextContent); const readingTime = Math.ceil(wordCount / 200); // 假设每分钟200词 // 创建文章 const post = await entityManager.create('Post', { blogId: input.blogId, title: input.data.title, slug, excerpt: input.data.excerpt, content: input.data.content, plainTextContent, wordCount, readingTime, featuredImage: input.data.featuredImage, featuredImageAlt: input.data.featuredImageAlt, categoryId: input.data.categoryId, authorId: user.id, status: input.data.status || 'DRAFT', visibility: input.data.visibility || 'PUBLIC', format: input.data.format || 'STANDARD', allowComments: input.data.allowComments !== false, seoTitle: input.data.seoTitle, seoDescription: input.data.seoDescription, seoKeywords: input.data.seoKeywords, socialTitle: input.data.socialTitle, socialDescription: input.data.socialDescription, socialImage: input.data.socialImage, customFields: input.data.customFields || {} }); // 处理标签 if (input.data.tags && input.data.tags.length > 0) { await this.assignTags(post.id, input.data.tags, entityManager); } // 处理协作作者 if (input.data.coAuthorIds && input.data.coAuthorIds.length > 0) { await this.assignCoAuthors(post.id, input.data.coAuthorIds, entityManager); } // 创建初始版本 await this.createRevision(post, user.id, 'Initial version', entityManager); return { success: true, data: { post }, events: [{ type: 'post.created', data: { postId: post.id, blogId: post.blogId, title: post.title, authorId: user.id } }] }; } private async publishPost(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { const post = await entityManager.get('Post', input.postId, { include: ['blog', 'category', 'tags'] }); if (!post) { throw new Error('Post not found'); } // 验证发布权限 if (!this.hasPublishPermission(user, post)) { throw new Error('Permission denied'); } // 验证发布条件 const validationResult = await this.validateForPublishing(post); if (!validationResult.valid) { throw new Error(`Publishing validation failed: ${validationResult.errors.join(', ')}`); } const publishedAt = new Date(); // 更新文章状态 const updatedPost = await entityManager.update('Post', input.postId, { status: 'PUBLISHED', publishedAt }); // 更新博客统计 await this.updateBlogStats(post.blogId, entityManager); // 更新分类统计 if (post.categoryId) { await this.updateCategoryStats(post.categoryId, entityManager); } // 更新标签统计 if (post.tags && post.tags.length > 0) { await this.updateTagStats(post.tags.map(t => t.id), entityManager); } // 发送通知给订阅者 await this.notifySubscribers(post, entityManager); // 生成sitemap await this.updateSitemap(post.blogId); // 触发SEO索引 await this.triggerSEOIndexing(post); return { success: true, data: { post: updatedPost }, events: [{ type: 'post.published', data: { postId: post.id, blogId: post.blogId, title: post.title, publishedAt, publishedBy: user.id } }] }; } private async schedulePost(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { const post = await entityManager.get('Post', input.postId); if (!post) { throw new Error('Post not found'); } if (!input.data.scheduledFor) { throw new Error('Scheduled date is required'); } const scheduledFor = new Date(input.data.scheduledFor); if (scheduledFor <= new Date()) { throw new Error('Scheduled date must be in the future'); } // 更新调度信息 const updatedPost = await entityManager.update('Post', input.postId, { status: 'DRAFT', scheduledFor }); // 创建调度任务 await this.createScheduleTask(post.id, scheduledFor); return { success: true, data: { post: updatedPost }, events: [{ type: 'post.scheduled', data: { postId: post.id, scheduledFor, scheduledBy: user.id } }] }; } private async generateUniqueSlug(title: string, blogId: string, entityManager: IEntityManager, excludeId?: string): Promise<string> { let baseSlug = title .toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/[\s_-]+/g, '-') .replace(/^-+|-+$/g, ''); let slug = baseSlug; let counter = 1; while (await this.slugExists(slug, blogId, entityManager, excludeId)) { slug = `${baseSlug}-${counter}`; counter++; } return slug; } private async slugExists(slug: string, blogId: string, entityManager: IEntityManager, excludeId?: string): Promise<boolean> { const filter: any = { blogId, slug }; if (excludeId) { filter.id = { $ne: excludeId }; } const existing = await entityManager.query({ entityType: 'Post', filter, limit: 1 }); return existing.data.length > 0; } private stripHtml(html: string): string { return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); } private countWords(text: string): number { return text.split(/\s+/).filter(word => word.length > 0).length; } private async assignTags(postId: string, tags: string[], entityManager: IEntityManager): Promise<void> { for (const tagName of tags) { const tag = await this.findOrCreateTag(tagName, entityManager); await entityManager.create('PostTag', { postId, tagId: tag.id }); } } private async findOrCreateTag(name: string, entityManager: IEntityManager): Promise<any> { const slug = name.toLowerCase().replace(/\s+/g, '-'); // 查找现有标签 const existing = await entityManager.query({ entityType: 'Tag', filter: { name }, limit: 1 }); if (existing.data.length > 0) { return existing.data[0]; } // 创建新标签 return await entityManager.create('Tag', { name, slug, postCount: 0 }); } private async assignCoAuthors(postId: string, coAuthorIds: string[], entityManager: IEntityManager): Promise<void> { for (const authorId of coAuthorIds) { await entityManager.create('PostCoAuthor', { postId, userId: authorId }); } } private async createRevision(post: any, userId: string, comment: string, entityManager: IEntityManager): Promise<any> { // 获取下一个版本号 const latestRevision = await entityManager.query({ entityType: 'PostRevision', filter: { postId: post.id }, sort: [{ field: 'version', direction: 'desc' }], limit: 1 }); const version = latestRevision.data.length > 0 ? latestRevision.data[0].version + 1 : 1; return await entityManager.create('PostRevision', { postId: post.id, version, title: post.title, content: post.content, excerpt: post.excerpt, createdById: userId, comment }); } private hasPublishPermission(user: any, post: any): boolean { return user.id === post.authorId || user.permissions.includes('publish_posts') || user.permissions.includes('manage_blog'); } private async validateForPublishing(post: any): Promise<ValidationResult> { const errors: string[] = []; if (!post.title || post.title.trim() === '') { errors.push('Title is required'); } if (!post.content || post.content.trim() === '') { errors.push('Content is required'); } if (!post.excerpt || post.excerpt.trim() === '') { errors.push('Excerpt is required'); } if (!post.seoTitle) { errors.push('SEO title is required'); } if (!post.seoDescription) { errors.push('SEO description is required'); } return { valid: errors.length === 0, errors, warnings: [] }; } private async updateBlogStats(blogId: string, entityManager: IEntityManager): Promise<void> { const postCount = await entityManager.query({ entityType: 'Post', filter: { blogId, status: 'PUBLISHED' }, operation: 'count' }); await entityManager.update('Blog', blogId, { postCount: postCount.total }); } private async updateCategoryStats(categoryId: string, entityManager: IEntityManager): Promise<void> { const postCount = await entityManager.query({ entityType: 'Post', filter: { categoryId, status: 'PUBLISHED' }, operation: 'count' }); await entityManager.update('Category', categoryId, { postCount: postCount.total }); } private async updateTagStats(tagIds: string[], entityManager: IEntityManager): Promise<void> { for (const tagId of tagIds) { const postCount = await entityManager.query({ entityType: 'PostTag', filter: { tagId }, operation: 'count' }); await entityManager.update('Tag', tagId, { postCount: postCount.total }); } } private async notifySubscribers(post: any, entityManager: IEntityManager): Promise<void> { // 获取活跃订阅者 const subscribers = await entityManager.query({ entityType: 'Subscriber', filter: { blogId: post.blogId, status: 'ACTIVE', isConfirmed: true } }); // 发送邮件通知(异步处理) for (const subscriber of subscribers.data) { await this.sendPostNotification(subscriber, post); } } private async sendPostNotification(subscriber: any, post: any): Promise<void> { // 实现邮件发送逻辑 } private async updateSitemap(blogId: string): Promise<void> { // 更新sitemap.xml } private async triggerSEOIndexing(post: any): Promise<void> { // 触发搜索引擎索引 } private async createScheduleTask(postId: string, scheduledFor: Date): Promise<void> { // 创建定时任务 } async validate(input: any): Promise<ValidationResult> { const errors: string[] = []; if (!input.action) { errors.push('action is required'); } if (input.action === 'create_post' && !input.blogId) { errors.push('blogId is required for create_post action'); } if (['update_post', 'publish_post', 'schedule_post', 'delete_post'].includes(input.action) && !input.postId) { errors.push('postId is required for this action'); } return { valid: errors.length === 0, errors, warnings: [] }; } getRequiredPermissions(): string[] { return ['blog_access']; } getMetadata(): ActionHandlerMetadata { return { async: false, timeout: 30000, retryable: true, maxRetries: 2 }; } }

评论管理动作处理器

// 评论管理动作处理器 class CommentManagerActionHandler implements IActionHandler { name = 'comment-manager'; displayName = '评论管理'; category = ActionCategory.BUSINESS; inputSchema: ActionInputSchema = { type: 'object', properties: { action: { type: 'string', enum: ['create', 'approve', 'reject', 'reply', 'moderate', 'delete'], required: true }, commentId: { type: 'string' }, postId: { type: 'string' }, data: { type: 'object' }, options: { type: 'object' } } }; async execute(context: ActionExecutionContext): Promise<ActionResult> { const { input, entityManager, user } = context; try { switch (input.action) { case 'create': return await this.createComment(input, entityManager, user); case 'approve': return await this.approveComment(input, entityManager, user); case 'reject': return await this.rejectComment(input, entityManager, user); case 'reply': return await this.replyToComment(input, entityManager, user); case 'moderate': return await this.moderateComment(input, entityManager, user); case 'delete': return await this.deleteComment(input, entityManager, user); default: throw new Error(`Unknown comment action: ${input.action}`); } } catch (error) { return { success: false, error: { message: error.message, code: 'COMMENT_ACTION_ERROR' } }; } } private async createComment(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { // 验证文章存在且允许评论 const post = await entityManager.get('Post', input.postId); if (!post) { throw new Error('Post not found'); } if (!post.allowComments) { throw new Error('Comments are disabled for this post'); } // 反垃圾检查 const spamCheckResult = await this.checkSpam(input.data, context); let status = 'PENDING'; if (spamCheckResult.isSpam) { status = 'SPAM'; } else if (this.isAutoApproveEnabled(post) && this.canAutoApprove(user, input.data)) { status = 'APPROVED'; } // 渲染HTML内容 const contentHtml = await this.renderCommentContent(input.data.content); // 创建评论 const comment = await entityManager.create('Comment', { postId: input.postId, parentId: input.data.parentId, content: input.data.content, contentHtml, authorName: input.data.authorName, authorEmail: input.data.authorEmail, authorUrl: input.data.authorUrl, userId: user?.id, status, ipAddress: input.data.ipAddress, userAgent: input.data.userAgent }); // 如果是回复,更新父评论统计 if (input.data.parentId) { await this.updateParentCommentStats(input.data.parentId, entityManager); } // 更新文章评论统计 await this.updatePostCommentStats(input.postId, entityManager); // 发送通知 if (status === 'APPROVED') { await this.sendCommentNotifications(comment, post, entityManager); } return { success: true, data: { comment }, events: [{ type: 'comment.created', data: { commentId: comment.id, postId: comment.postId, status: comment.status, isReply: !!comment.parentId } }] }; } private async approveComment(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { const comment = await entityManager.get('Comment', input.commentId, { include: ['post'] }); if (!comment) { throw new Error('Comment not found'); } if (comment.status === 'APPROVED') { throw new Error('Comment is already approved'); } // 更新评论状态 const updatedComment = await entityManager.update('Comment', input.commentId, { status: 'APPROVED' }); // 更新统计 await this.updatePostCommentStats(comment.postId, entityManager); // 发送通知 await this.sendCommentNotifications(updatedComment, comment.post, entityManager); return { success: true, data: { comment: updatedComment }, events: [{ type: 'comment.approved', data: { commentId: comment.id, postId: comment.postId, approvedBy: user.id } }] }; } private async moderateComment(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { const comment = await entityManager.get('Comment', input.commentId); if (!comment) { throw new Error('Comment not found'); } const action = input.data.action; // 'approve', 'reject', 'spam' const reason = input.data.reason; let newStatus: string; switch (action) { case 'approve': newStatus = 'APPROVED'; break; case 'reject': newStatus = 'REJECTED'; break; case 'spam': newStatus = 'SPAM'; break; default: throw new Error(`Invalid moderation action: ${action}`); } // 更新评论状态 const updatedComment = await entityManager.update('Comment', input.commentId, { status: newStatus, moderationReason: reason }); // 如果标记为垃圾,更新垃圾检测系统 if (newStatus === 'SPAM') { await this.updateSpamDetection(comment, true); } // 更新统计 await this.updatePostCommentStats(comment.postId, entityManager); return { success: true, data: { comment: updatedComment }, events: [{ type: 'comment.moderated', data: { commentId: comment.id, action, reason, moderatedBy: user.id } }] }; } private async checkSpam(commentData: any, context: any): Promise<{ isSpam: boolean; confidence: number; reasons: string[] }> { const reasons: string[] = []; let spamScore = 0; // 检查黑名单关键词 const spamKeywords = ['viagra', 'casino', 'lottery', 'buy now', 'click here']; const content = commentData.content.toLowerCase(); for (const keyword of spamKeywords) { if (content.includes(keyword)) { spamScore += 30; reasons.push(`Contains spam keyword: ${keyword}`); } } // 检查链接数量 const linkCount = (commentData.content.match(/https?:\/\//g) || []).length; if (linkCount > 3) { spamScore += 25; reasons.push(`Too many links: ${linkCount}`); } // 检查重复评论 const duplicateCheck = await this.checkDuplicateComment(commentData); if (duplicateCheck.isDuplicate) { spamScore += 40; reasons.push('Duplicate comment detected'); } // 检查IP声誉 const ipReputation = await this.checkIPReputation(commentData.ipAddress); if (ipReputation.isBlacklisted) { spamScore += 50; reasons.push('IP address is blacklisted'); } return { isSpam: spamScore >= 50, confidence: Math.min(spamScore, 100), reasons }; } private async renderCommentContent(content: string): Promise<string> { // 简单的Markdown渲染和XSS过滤 return content .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/`(.*?)`/g, '<code>$1</code>') .replace(/\n/g, '<br>'); } private isAutoApproveEnabled(post: any): boolean { // 检查文章或博客的自动批准设置 return post.blog?.settings?.autoApproveComments || false; } private canAutoApprove(user: any, commentData: any): boolean { // 检查用户是否可以自动批准 if (user) { // 已登录用户 return user.trustLevel >= 1; } // 匿名用户的启发式规则 if (commentData.authorUrl && !this.isValidUrl(commentData.authorUrl)) { return false; } // 内容长度和质量检查 if (commentData.content.length < 10) { return false; } return true; } private async updateParentCommentStats(parentId: string, entityManager: IEntityManager): Promise<void> { const replyCount = await entityManager.query({ entityType: 'Comment', filter: { parentId, status: 'APPROVED' }, operation: 'count' }); await entityManager.update('Comment', parentId, { replyCount: replyCount.total }); } private async updatePostCommentStats(postId: string, entityManager: IEntityManager): Promise<void> { const commentCount = await entityManager.query({ entityType: 'Comment', filter: { postId, status: 'APPROVED' }, operation: 'count' }); await entityManager.update('Post', postId, { commentCount: commentCount.total }); } private async sendCommentNotifications(comment: any, post: any, entityManager: IEntityManager): Promise<void> { // 通知文章作者 if (comment.userId !== post.authorId) { await this.sendNotificationToAuthor(comment, post); } // 如果是回复,通知父评论作者 if (comment.parentId) { const parentComment = await entityManager.get('Comment', comment.parentId); if (parentComment && parentComment.userId && parentComment.userId !== comment.userId) { await this.sendNotificationToCommenter(comment, parentComment); } } } private async sendNotificationToAuthor(comment: any, post: any): Promise<void> { // 实现作者通知逻辑 } private async sendNotificationToCommenter(comment: any, parentComment: any): Promise<void> { // 实现回复通知逻辑 } private async checkDuplicateComment(commentData: any): Promise<{ isDuplicate: boolean }> { // 检查重复评论逻辑 return { isDuplicate: false }; } private async checkIPReputation(ipAddress: string): Promise<{ isBlacklisted: boolean }> { // 检查IP声誉逻辑 return { isBlacklisted: false }; } private async updateSpamDetection(comment: any, isSpam: boolean): Promise<void> { // 更新垃圾检测系统 } private isValidUrl(url: string): boolean { try { new URL(url); return true; } catch { return false; } } async validate(input: any): Promise<ValidationResult> { const errors: string[] = []; if (!input.action) { errors.push('action is required'); } if (input.action === 'create' && !input.postId) { errors.push('postId is required for create action'); } if (['approve', 'reject', 'moderate', 'delete'].includes(input.action) && !input.commentId) { errors.push('commentId is required for this action'); } return { valid: errors.length === 0, errors, warnings: [] }; } getRequiredPermissions(): string[] { return ['comment_access']; } getMetadata(): ActionHandlerMetadata { return { async: false, timeout: 15000, retryable: true, maxRetries: 2 }; } }

视图定义

博客仪表盘视图

// 博客仪表盘视图 export const BlogDashboardView: IViewDefinition = { id: 'blog-dashboard', name: 'BlogDashboard', displayName: '博客仪表盘', type: 'dashboard', config: { layout: { type: 'grid', columns: 12, rows: 'auto' }, widgets: [ { id: 'content-stats', type: 'stats-overview', title: '内容统计', position: { x: 0, y: 0, w: 12, h: 2 }, config: { metrics: [ { name: 'total_posts', displayName: '总文章数', query: { entityType: 'Post', filter: { status: 'PUBLISHED' }, aggregation: { operation: 'count' } }, icon: 'document', color: 'blue' }, { name: 'total_views', displayName: '总浏览量', query: { entityType: 'Post', aggregation: { field: 'viewCount', operation: 'sum' } }, icon: 'eye', color: 'green' }, { name: 'total_comments', displayName: '总评论数', query: { entityType: 'Comment', filter: { status: 'APPROVED' }, aggregation: { operation: 'count' } }, icon: 'chat', color: 'purple' }, { name: 'subscribers', displayName: '订阅者', query: { entityType: 'Subscriber', filter: { status: 'ACTIVE' }, aggregation: { operation: 'count' } }, icon: 'users', color: 'orange' } ] } }, { id: 'recent-posts', type: 'post-list', title: '最新文章', position: { x: 0, y: 2, w: 8, h: 4 }, config: { query: { entityType: 'Post', sort: [{ field: 'createdAt', direction: 'desc' }], limit: 5, include: ['author', 'category'] }, showThumbnail: true, showStats: true, showStatus: true } }, { id: 'popular-posts', type: 'post-list', title: '热门文章', position: { x: 8, y: 2, w: 4, h: 4 }, config: { query: { entityType: 'Post', filter: { status: 'PUBLISHED' }, sort: [{ field: 'viewCount', direction: 'desc' }], limit: 5 }, showViews: true, compact: true } }, { id: 'comment-moderation', type: 'comment-queue', title: '待审核评论', position: { x: 0, y: 6, w: 6, h: 3 }, config: { query: { entityType: 'Comment', filter: { status: 'PENDING' }, sort: [{ field: 'createdAt', direction: 'desc' }], include: ['post'] }, showModerationActions: true } }, { id: 'analytics-chart', type: 'analytics-chart', title: '浏览量趋势', position: { x: 6, y: 6, w: 6, h: 3 }, config: { metric: 'page_views', period: '7d', chartType: 'line', showComparison: true } } ], refreshInterval: 300000 } }; // 文章列表视图 export const PostListView: IViewDefinition = { id: 'post-list', name: 'PostList', displayName: '文章列表', type: 'table', entityType: 'Post', config: { columns: [ { name: 'featuredImage', displayName: '图片', type: 'image', width: 80, renderer: 'post-thumbnail' }, { name: 'title', displayName: '标题', type: 'string', sortable: true, searchable: true, width: 300, renderer: 'post-title-link' }, { name: 'author.name', displayName: '作者', type: 'string', sortable: true, width: 120 }, { name: 'category.name', displayName: '分类', type: 'string', sortable: true, filterable: true, width: 120 }, { name: 'status', displayName: '状态', type: 'enum', sortable: true, filterable: true, width: 100, renderer: 'post-status-badge' }, { name: 'viewCount', displayName: '浏览量', type: 'number', sortable: true, width: 80 }, { name: 'commentCount', displayName: '评论数', type: 'number', sortable: true, width: 80 }, { name: 'publishedAt', displayName: '发布时间', type: 'datetime', sortable: true, width: 140, format: { dateStyle: 'short', timeStyle: 'short' } } ], defaultSort: [{ field: 'createdAt', direction: 'desc' }], pageSize: 25, enableSearch: true, enableFilters: true, filters: [ { name: 'status', displayName: '状态', type: 'select', options: [ { label: '草稿', value: 'DRAFT' }, { label: '待审核', value: 'PENDING' }, { label: '已发布', value: 'PUBLISHED' }, { label: '已归档', value: 'ARCHIVED' } ] }, { name: 'category', displayName: '分类', type: 'select', dataSource: 'Category', labelField: 'name', valueField: 'id' }, { name: 'author', displayName: '作者', type: 'select', dataSource: 'User', labelField: 'name', valueField: 'id' } ], actions: [ { name: 'create', displayName: '新建文章', type: 'primary', icon: 'plus', handler: 'navigate-to-create' }, { name: 'bulk-publish', displayName: '批量发布', type: 'secondary', icon: 'publish', handler: 'bulk-publish', requiresSelection: true } ], rowActions: [ { name: 'edit', displayName: '编辑', icon: 'edit', handler: 'navigate-to-edit' }, { name: 'preview', displayName: '预览', icon: 'eye', handler: 'preview-post' }, { name: 'publish', displayName: '发布', icon: 'publish', handler: 'publish-post', condition: { status: ['DRAFT', 'PENDING'] } }, { name: 'duplicate', displayName: '复制', icon: 'copy', handler: 'duplicate-post' } ] } }; // 文章编辑器视图 export const PostEditorView: IViewDefinition = { id: 'post-editor', name: 'PostEditor', displayName: '文章编辑器', type: 'custom', entityType: 'Post', config: { component: 'PostEditor', layout: { type: 'split', panels: [ { id: 'main-editor', size: '70%', sections: [ { id: 'title-section', type: 'title-input', config: { placeholder: '请输入文章标题...', autoGenerateSlug: true } }, { id: 'content-editor', type: 'rich-text-editor', config: { toolbar: 'full', plugins: ['image', 'video', 'code', 'table', 'link'], autoSave: true, autoSaveInterval: 30000 } } ] }, { id: 'sidebar', size: '30%', sections: [ { id: 'publish-box', type: 'publish-controls', title: '发布', collapsible: false }, { id: 'featured-image', type: 'media-selector', title: '特色图片', collapsible: true }, { id: 'excerpt', type: 'textarea', title: '摘要', collapsible: true, config: { rows: 4, placeholder: '请输入文章摘要...' } }, { id: 'categorization', type: 'taxonomy', title: '分类和标签', collapsible: true }, { id: 'seo-settings', type: 'seo-panel', title: 'SEO设置', collapsible: true } ] } ] }, autosave: { enabled: true, interval: 30000 }, keyboard: { 'Ctrl+S': 'save-draft', 'Ctrl+Shift+P': 'publish', 'Ctrl+P': 'preview' } } };

下一步

完成博客系统的基础架构后,可以继续学习:

Last updated on