Skip to Content

内容管理系统

本文介绍如何使用 Entity Engine 构建一个功能完整的内容管理系统(CMS),涵盖内容创建、编辑、发布、多媒体管理、SEO优化和用户权限控制等核心功能。

系统架构

核心实体模型

// 内容类型模型 export const ContentTypeModel: IEntityModel = { name: 'ContentType', fields: { id: { type: 'uuid', primaryKey: true }, name: { type: 'string', required: true, unique: true }, slug: { type: 'string', required: true, unique: true }, displayName: { type: 'string', required: true }, description: { type: 'string' }, icon: { type: 'string' }, category: { type: 'enum', enumValues: ['PAGE', 'POST', 'MEDIA', 'FORM', 'CUSTOM'], defaultValue: 'CUSTOM' }, schema: { type: 'json', required: true }, // 字段定义 templates: { type: 'json' }, // 模板配置 permissions: { type: 'json' }, // 权限配置 isBuiltIn: { type: 'boolean', defaultValue: false }, isActive: { type: 'boolean', defaultValue: true }, sortOrder: { type: 'number', defaultValue: 0 }, contents: { type: 'relation', relation: { type: 'one_to_many', model: 'Content', foreignKey: 'contentTypeId' } }, createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['slug'], unique: true }, { fields: ['category'] }, { fields: ['isActive', 'sortOrder'] } ] }; // 内容模型 export const ContentModel: IEntityModel = { name: 'Content', fields: { id: { type: 'uuid', primaryKey: true }, contentTypeId: { type: 'uuid', required: true }, contentType: { type: 'relation', relation: { type: 'many_to_one', model: 'ContentType', targetKey: 'id' } }, title: { type: 'string', required: true }, slug: { type: 'string', required: true }, excerpt: { type: 'string' }, content: { type: 'text' }, metadata: { type: 'json' }, // 动态字段数据 status: { type: 'enum', enumValues: ['DRAFT', 'PENDING', 'PUBLISHED', 'ARCHIVED'], defaultValue: 'DRAFT' }, visibility: { type: 'enum', enumValues: ['PUBLIC', 'PRIVATE', 'PASSWORD', 'MEMBERS_ONLY'], defaultValue: 'PUBLIC' }, password: { type: 'string' }, publishedAt: { type: 'datetime' }, expiresAt: { type: 'datetime' }, authorId: { type: 'uuid', required: true }, author: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, editorId: { type: 'uuid' }, editor: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, parentId: { type: 'uuid' }, parent: { type: 'relation', relation: { type: 'many_to_one', model: 'Content', targetKey: 'id' } }, children: { type: 'relation', relation: { type: 'one_to_many', model: 'Content', foreignKey: 'parentId' } }, featuredImage: { type: 'string' }, gallery: { type: 'json' }, // 图片集 tags: { type: 'relation', relation: { type: 'many_to_many', model: 'Tag', through: 'ContentTag' } }, categories: { type: 'relation', relation: { type: 'many_to_many', model: 'Category', through: 'ContentCategory' } }, versions: { type: 'relation', relation: { type: 'one_to_many', model: 'ContentVersion', foreignKey: 'contentId' } }, comments: { type: 'relation', relation: { type: 'one_to_many', model: 'Comment', foreignKey: 'contentId' } }, // SEO字段 seoTitle: { type: 'string' }, seoDescription: { type: 'string' }, seoKeywords: { type: 'string' }, socialImage: { type: 'string' }, // 统计字段 viewCount: { type: 'number', defaultValue: 0 }, likeCount: { type: 'number', defaultValue: 0 }, commentCount: { type: 'number', defaultValue: 0 }, shareCount: { type: 'number', defaultValue: 0 }, // 时间戳 createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['contentTypeId', 'slug'], unique: true }, { fields: ['status'] }, { fields: ['publishedAt'] }, { fields: ['authorId'] }, { fields: ['parentId'] }, { fields: ['createdAt'] } ] }; // 内容版本模型 export const ContentVersionModel: IEntityModel = { name: 'ContentVersion', fields: { id: { type: 'uuid', primaryKey: true }, contentId: { type: 'uuid', required: true }, content: { type: 'relation', relation: { type: 'many_to_one', model: 'Content', targetKey: 'id' } }, versionNumber: { type: 'number', required: true }, title: { type: 'string', required: true }, excerpt: { type: 'string' }, contentData: { type: 'text' }, metadata: { type: 'json' }, changes: { type: 'json' }, // 变更摘要 changeType: { type: 'enum', enumValues: ['MINOR', 'MAJOR', 'PATCH'], defaultValue: 'MINOR' }, createdById: { type: 'uuid', required: true }, createdBy: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, comment: { type: 'string' }, isAutoSave: { type: 'boolean', defaultValue: false }, createdAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['contentId', 'versionNumber'], unique: true }, { fields: ['contentId', 'createdAt'] }, { fields: ['createdById'] } ] }; // 分类模型 export const CategoryModel: IEntityModel = { name: 'Category', fields: { id: { type: 'uuid', primaryKey: true }, name: { type: 'string', required: true }, slug: { type: 'string', required: true, unique: true }, description: { type: 'string' }, color: { type: 'string' }, icon: { 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' } }, contents: { type: 'relation', relation: { type: 'many_to_many', model: 'Content', through: 'ContentCategory' } }, sortOrder: { type: 'number', defaultValue: 0 }, isActive: { type: 'boolean', defaultValue: true }, createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['slug'], unique: true }, { fields: ['parentId'] }, { fields: ['isActive', 'sortOrder'] } ] }; // 标签模型 export const TagModel: IEntityModel = { name: 'Tag', fields: { id: { type: 'uuid', primaryKey: true }, name: { type: 'string', required: true, unique: true }, slug: { type: 'string', required: true, unique: true }, description: { type: 'string' }, color: { type: 'string' }, contents: { type: 'relation', relation: { type: 'many_to_many', model: 'Content', through: 'ContentTag' } }, usageCount: { type: 'number', defaultValue: 0 }, createdAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['name'], unique: true }, { fields: ['slug'], unique: true }, { fields: ['usageCount'] } ] }; // 媒体文件模型 export const MediaModel: IEntityModel = { name: 'Media', fields: { id: { type: 'uuid', primaryKey: true }, filename: { type: 'string', required: true }, originalName: { type: 'string', required: true }, mimeType: { type: 'string', required: true }, size: { type: 'number', required: true }, width: { type: 'number' }, height: { type: 'number' }, duration: { type: 'number' }, // 视频/音频时长 url: { type: 'string', required: true }, thumbnailUrl: { type: 'string' }, alt: { type: 'string' }, caption: { type: 'string' }, description: { type: 'string' }, category: { type: 'enum', enumValues: ['IMAGE', 'VIDEO', 'AUDIO', 'DOCUMENT', 'OTHER'], required: true }, folderId: { type: 'uuid' }, folder: { type: 'relation', relation: { type: 'many_to_one', model: 'MediaFolder', targetKey: 'id' } }, uploadedById: { type: 'uuid', required: true }, uploadedBy: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, metadata: { type: 'json' }, // EXIF等元数据 tags: { type: 'json' }, isPublic: { type: 'boolean', defaultValue: true }, downloadCount: { type: 'number', defaultValue: 0 }, createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['category'] }, { fields: ['mimeType'] }, { fields: ['folderId'] }, { fields: ['uploadedById'] }, { fields: ['createdAt'] } ] }; // 媒体文件夹模型 export const MediaFolderModel: IEntityModel = { name: 'MediaFolder', fields: { id: { type: 'uuid', primaryKey: true }, name: { type: 'string', required: true }, description: { type: 'string' }, parentId: { type: 'uuid' }, parent: { type: 'relation', relation: { type: 'many_to_one', model: 'MediaFolder', targetKey: 'id' } }, children: { type: 'relation', relation: { type: 'one_to_many', model: 'MediaFolder', foreignKey: 'parentId' } }, media: { type: 'relation', relation: { type: 'one_to_many', model: 'Media', foreignKey: 'folderId' } }, createdById: { type: 'uuid', required: true }, createdBy: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['parentId', 'name'], unique: true }, { fields: ['createdById'] } ] }; // 评论模型 export const CommentModel: IEntityModel = { name: 'Comment', fields: { id: { type: 'uuid', primaryKey: true }, contentId: { type: 'uuid', required: true }, content: { type: 'relation', relation: { type: 'many_to_one', model: 'Content', 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' } }, authorName: { type: 'string', required: true }, authorEmail: { type: 'string', required: true }, authorUrl: { type: 'string' }, authorIP: { type: 'string' }, authorUserAgent: { type: 'string' }, userId: { type: 'uuid' }, user: { type: 'relation', relation: { type: 'many_to_one', model: 'User', targetKey: 'id' } }, commentText: { type: 'text', required: true }, status: { type: 'enum', enumValues: ['PENDING', 'APPROVED', 'REJECTED', 'SPAM'], defaultValue: 'PENDING' }, isAnonymous: { type: 'boolean', defaultValue: false }, likeCount: { type: 'number', defaultValue: 0 }, replyCount: { type: 'number', defaultValue: 0 }, createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['contentId'] }, { fields: ['parentId'] }, { fields: ['status'] }, { fields: ['userId'] }, { fields: ['createdAt'] } ] }; // 菜单模型 export const MenuModel: IEntityModel = { name: 'Menu', fields: { id: { type: 'uuid', primaryKey: true }, name: { type: 'string', required: true }, location: { type: 'string', required: true }, description: { type: 'string' }, items: { type: 'relation', relation: { type: 'one_to_many', model: 'MenuItem', foreignKey: 'menuId' } }, isActive: { type: 'boolean', defaultValue: true }, createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['location'], unique: true }, { fields: ['isActive'] } ] }; // 菜单项模型 export const MenuItemModel: IEntityModel = { name: 'MenuItem', fields: { id: { type: 'uuid', primaryKey: true }, menuId: { type: 'uuid', required: true }, menu: { type: 'relation', relation: { type: 'many_to_one', model: 'Menu', targetKey: 'id' } }, parentId: { type: 'uuid' }, parent: { type: 'relation', relation: { type: 'many_to_one', model: 'MenuItem', targetKey: 'id' } }, children: { type: 'relation', relation: { type: 'one_to_many', model: 'MenuItem', foreignKey: 'parentId' } }, title: { type: 'string', required: true }, url: { type: 'string' }, contentId: { type: 'uuid' }, targetContent: { type: 'relation', relation: { type: 'many_to_one', model: 'Content', targetKey: 'id' } }, icon: { type: 'string' }, cssClass: { type: 'string' }, target: { type: 'enum', enumValues: ['_self', '_blank', '_parent', '_top'], defaultValue: '_self' }, sortOrder: { type: 'number', defaultValue: 0 }, isActive: { type: 'boolean', defaultValue: true }, createdAt: { type: 'datetime', defaultValue: 'now' }, updatedAt: { type: 'datetime', defaultValue: 'now' } }, indexes: [ { fields: ['menuId', 'sortOrder'] }, { fields: ['parentId'] }, { fields: ['contentId'] } ] };

业务逻辑实现

内容管理动作处理器

// 内容操作动作处理器 class ContentActionHandler implements IActionHandler { name = 'content-manager'; displayName = '内容管理'; category = ActionCategory.BUSINESS; inputSchema: ActionInputSchema = { type: 'object', properties: { action: { type: 'string', enum: ['create', 'update', 'publish', 'unpublish', 'archive', 'delete', 'duplicate'], required: true }, contentId: { type: 'string' }, contentTypeId: { 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.createContent(input, entityManager, user); case 'update': return await this.updateContent(input, entityManager, user); case 'publish': return await this.publishContent(input, entityManager, user); case 'unpublish': return await this.unpublishContent(input, entityManager, user); case 'archive': return await this.archiveContent(input, entityManager, user); case 'delete': return await this.deleteContent(input, entityManager, user); case 'duplicate': return await this.duplicateContent(input, entityManager, user); default: throw new Error(`Unknown content action: ${input.action}`); } } catch (error) { return { success: false, error: { message: error.message, code: 'CONTENT_ACTION_ERROR' } }; } } private async createContent(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { // 验证内容类型 const contentType = await entityManager.get('ContentType', input.contentTypeId); if (!contentType || !contentType.isActive) { throw new Error('Invalid or inactive content type'); } // 验证数据结构 const validationResult = await this.validateContentData(input.data, contentType.schema); if (!validationResult.valid) { throw new Error(`Data validation failed: ${validationResult.errors.join(', ')}`); } // 生成唯一slug const slug = await this.generateUniqueSlug(input.data.title, input.contentTypeId, entityManager); // 创建内容 const content = await entityManager.create('Content', { contentTypeId: input.contentTypeId, title: input.data.title, slug, excerpt: input.data.excerpt, content: input.data.content, metadata: input.data.metadata || {}, status: input.data.status || 'DRAFT', visibility: input.data.visibility || 'PUBLIC', authorId: user.id, featuredImage: input.data.featuredImage, gallery: input.data.gallery || [], seoTitle: input.data.seoTitle, seoDescription: input.data.seoDescription, seoKeywords: input.data.seoKeywords, socialImage: input.data.socialImage }); // 处理分类和标签 if (input.data.categoryIds) { await this.assignCategories(content.id, input.data.categoryIds, entityManager); } if (input.data.tags) { await this.assignTags(content.id, input.data.tags, entityManager); } // 创建初始版本 await this.createVersion(content, user.id, 'Initial version', entityManager); return { success: true, data: { content }, events: [{ type: 'content.created', data: { contentId: content.id, contentType: contentType.name, title: content.title, authorId: user.id } }] }; } private async updateContent(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { const content = await entityManager.get('Content', input.contentId, { include: ['contentType', 'versions'] }); if (!content) { throw new Error('Content not found'); } // 权限检查 if (content.authorId !== user.id && !this.hasEditPermission(user, content)) { throw new Error('Permission denied'); } // 创建版本备份 if (input.options?.createVersion !== false) { await this.createVersion(content, user.id, input.options?.versionComment, entityManager); } // 验证数据 if (input.data.metadata) { const validationResult = await this.validateContentData( { ...content.metadata, ...input.data.metadata }, content.contentType.schema ); if (!validationResult.valid) { throw new Error(`Data validation failed: ${validationResult.errors.join(', ')}`); } } // 更新slug(如果标题改变) let slug = content.slug; if (input.data.title && input.data.title !== content.title) { slug = await this.generateUniqueSlug(input.data.title, content.contentTypeId, entityManager, content.id); } // 更新内容 const updatedContent = await entityManager.update('Content', input.contentId, { title: input.data.title || content.title, slug, excerpt: input.data.excerpt !== undefined ? input.data.excerpt : content.excerpt, content: input.data.content !== undefined ? input.data.content : content.content, metadata: { ...content.metadata, ...input.data.metadata }, featuredImage: input.data.featuredImage !== undefined ? input.data.featuredImage : content.featuredImage, gallery: input.data.gallery || content.gallery, seoTitle: input.data.seoTitle !== undefined ? input.data.seoTitle : content.seoTitle, seoDescription: input.data.seoDescription !== undefined ? input.data.seoDescription : content.seoDescription, seoKeywords: input.data.seoKeywords !== undefined ? input.data.seoKeywords : content.seoKeywords, socialImage: input.data.socialImage !== undefined ? input.data.socialImage : content.socialImage, editorId: user.id }); // 更新分类和标签 if (input.data.categoryIds) { await this.updateCategories(content.id, input.data.categoryIds, entityManager); } if (input.data.tags) { await this.updateTags(content.id, input.data.tags, entityManager); } return { success: true, data: { content: updatedContent }, events: [{ type: 'content.updated', data: { contentId: content.id, changes: this.getChanges(content, input.data), editorId: user.id } }] }; } private async publishContent(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { const content = await entityManager.get('Content', input.contentId); if (!content) { throw new Error('Content not found'); } // 权限检查 if (!this.hasPublishPermission(user, content)) { throw new Error('Permission denied'); } // 验证发布条件 const validationResult = await this.validateForPublishing(content, entityManager); if (!validationResult.valid) { throw new Error(`Publishing validation failed: ${validationResult.errors.join(', ')}`); } const publishedAt = new Date(); // 更新状态 const updatedContent = await entityManager.update('Content', input.contentId, { status: 'PUBLISHED', publishedAt, editorId: user.id }); // 触发发布后处理 await this.processPostPublish(updatedContent, entityManager); return { success: true, data: { content: updatedContent }, events: [{ type: 'content.published', data: { contentId: content.id, title: content.title, publishedAt, publishedBy: user.id } }] }; } private async validateContentData(data: any, schema: any): Promise<ValidationResult> { const errors: string[] = []; // 基于schema验证数据结构 for (const field of schema.fields) { if (field.required && (data[field.name] === undefined || data[field.name] === null)) { errors.push(`Field '${field.name}' is required`); } if (data[field.name] !== undefined) { const fieldValidation = await this.validateField(data[field.name], field); if (!fieldValidation.valid) { errors.push(...fieldValidation.errors); } } } return { valid: errors.length === 0, errors, warnings: [] }; } private async validateField(value: any, field: any): Promise<ValidationResult> { const errors: string[] = []; switch (field.type) { case 'string': if (typeof value !== 'string') { errors.push(`Field '${field.name}' must be a string`); } else if (field.maxLength && value.length > field.maxLength) { errors.push(`Field '${field.name}' exceeds maximum length of ${field.maxLength}`); } break; case 'number': if (typeof value !== 'number' || isNaN(value)) { errors.push(`Field '${field.name}' must be a valid number`); } else { if (field.min !== undefined && value < field.min) { errors.push(`Field '${field.name}' must be at least ${field.min}`); } if (field.max !== undefined && value > field.max) { errors.push(`Field '${field.name}' must be at most ${field.max}`); } } break; case 'email': if (!this.isValidEmail(value)) { errors.push(`Field '${field.name}' must be a valid email address`); } break; case 'url': if (!this.isValidUrl(value)) { errors.push(`Field '${field.name}' must be a valid URL`); } break; case 'array': if (!Array.isArray(value)) { errors.push(`Field '${field.name}' must be an array`); } else { if (field.minItems && value.length < field.minItems) { errors.push(`Field '${field.name}' must have at least ${field.minItems} items`); } if (field.maxItems && value.length > field.maxItems) { errors.push(`Field '${field.name}' must have at most ${field.maxItems} items`); } } break; } return { valid: errors.length === 0, errors, warnings: [] }; } private async generateUniqueSlug( title: string, contentTypeId: 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, contentTypeId, entityManager, excludeId)) { slug = `${baseSlug}-${counter}`; counter++; } return slug; } private async slugExists( slug: string, contentTypeId: string, entityManager: IEntityManager, excludeId?: string ): Promise<boolean> { const filter: any = { contentTypeId, slug }; if (excludeId) { filter.id = { $ne: excludeId }; } const existing = await entityManager.query({ entityType: 'Content', filter, limit: 1 }); return existing.data.length > 0; } private async createVersion( content: any, userId: string, comment: string, entityManager: IEntityManager ): Promise<any> { // 获取下一个版本号 const latestVersion = await entityManager.query({ entityType: 'ContentVersion', filter: { contentId: content.id }, sort: [{ field: 'versionNumber', direction: 'desc' }], limit: 1 }); const versionNumber = latestVersion.data.length > 0 ? latestVersion.data[0].versionNumber + 1 : 1; return await entityManager.create('ContentVersion', { contentId: content.id, versionNumber, title: content.title, excerpt: content.excerpt, contentData: content.content, metadata: content.metadata, createdById: userId, comment: comment || 'Auto-saved version' }); } private async assignCategories( contentId: string, categoryIds: string[], entityManager: IEntityManager ): Promise<void> { // 删除现有分类关联 await entityManager.query({ entityType: 'ContentCategory', filter: { contentId }, operation: 'delete' }); // 创建新的分类关联 for (const categoryId of categoryIds) { await entityManager.create('ContentCategory', { contentId, categoryId }); } } private async assignTags( contentId: string, tags: string[], entityManager: IEntityManager ): Promise<void> { // 删除现有标签关联 await entityManager.query({ entityType: 'ContentTag', filter: { contentId }, operation: 'delete' }); // 处理标签(创建新标签或使用现有标签) for (const tagName of tags) { let tag = await this.findOrCreateTag(tagName, entityManager); await entityManager.create('ContentTag', { contentId, 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) { // 更新使用计数 await entityManager.update('Tag', existing.data[0].id, { usageCount: existing.data[0].usageCount + 1 }); return existing.data[0]; } // 创建新标签 return await entityManager.create('Tag', { name, slug, usageCount: 1 }); } private hasEditPermission(user: any, content: any): boolean { return user.permissions.includes('edit_all_content') || user.permissions.includes(`edit_content:${content.contentType.name}`); } private hasPublishPermission(user: any, content: any): boolean { return user.permissions.includes('publish_content') || user.permissions.includes(`publish_content:${content.contentType.name}`); } private async validateForPublishing(content: any, entityManager: IEntityManager): Promise<ValidationResult> { const errors: string[] = []; if (!content.title || content.title.trim() === '') { errors.push('Title is required for publishing'); } if (!content.content || content.content.trim() === '') { errors.push('Content is required for publishing'); } // 验证SEO字段 if (!content.seoTitle) { errors.push('SEO title is required for publishing'); } if (!content.seoDescription) { errors.push('SEO description is required for publishing'); } return { valid: errors.length === 0, errors, warnings: [] }; } private async processPostPublish(content: any, entityManager: IEntityManager): Promise<void> { // 更新统计 await this.updateContentStats(content.id, entityManager); // 清理缓存 await this.clearContentCache(content.id); // 触发搜索引擎索引 await this.triggerSearchIndexing(content); } private getChanges(oldContent: any, newData: any): any[] { const changes: any[] = []; for (const [key, value] of Object.entries(newData)) { if (oldContent[key] !== value) { changes.push({ field: key, oldValue: oldContent[key], newValue: value }); } } return changes; } private isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } private isValidUrl(url: string): boolean { try { new URL(url); return true; } catch { return false; } } private async updateContentStats(contentId: string, entityManager: IEntityManager): Promise<void> { // 实现统计更新 } private async clearContentCache(contentId: string): Promise<void> { // 实现缓存清理 } private async triggerSearchIndexing(content: any): Promise<void> { // 实现搜索引擎索引触发 } async validate(input: any): Promise<ValidationResult> { const errors: string[] = []; if (!input.action) { errors.push('action is required'); } if (['update', 'publish', 'unpublish', 'archive', 'delete', 'duplicate'].includes(input.action) && !input.contentId) { errors.push('contentId is required for this action'); } if (input.action === 'create' && !input.contentTypeId) { errors.push('contentTypeId is required for create action'); } return { valid: errors.length === 0, errors, warnings: [] }; } getRequiredPermissions(): string[] { return ['content_access']; } getMetadata(): ActionHandlerMetadata { return { async: false, timeout: 30000, retryable: true, maxRetries: 2 }; } }

媒体管理动作处理器

// 媒体管理动作处理器 class MediaManagerActionHandler implements IActionHandler { name = 'media-manager'; displayName = '媒体管理'; category = ActionCategory.BUSINESS; inputSchema: ActionInputSchema = { type: 'object', properties: { action: { type: 'string', enum: ['upload', 'update', 'delete', 'organize', 'resize', 'optimize'], required: true }, mediaId: { type: 'string' }, file: { type: 'object' }, folderId: { type: 'string' }, metadata: { type: 'object' }, options: { type: 'object' } } }; private storageProvider: IStorageProvider; private imageProcessor: IImageProcessor; async initialize(context: ActionHandlerContext): Promise<void> { this.storageProvider = new CloudStorageProvider(); this.imageProcessor = new SharpImageProcessor(); await this.storageProvider.initialize(); await this.imageProcessor.initialize(); } async execute(context: ActionExecutionContext): Promise<ActionResult> { const { input, entityManager, user } = context; try { switch (input.action) { case 'upload': return await this.uploadMedia(input, entityManager, user); case 'update': return await this.updateMedia(input, entityManager, user); case 'delete': return await this.deleteMedia(input, entityManager, user); case 'organize': return await this.organizeMedia(input, entityManager, user); case 'resize': return await this.resizeMedia(input, entityManager, user); case 'optimize': return await this.optimizeMedia(input, entityManager, user); default: throw new Error(`Unknown media action: ${input.action}`); } } catch (error) { return { success: false, error: { message: error.message, code: 'MEDIA_ACTION_ERROR' } }; } } private async uploadMedia(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> { const file = input.file; // 验证文件类型和大小 const validationResult = await this.validateFile(file); if (!validationResult.valid) { throw new Error(`File validation failed: ${validationResult.errors.join(', ')}`); } // 生成文件名 const filename = this.generateUniqueFilename(file.originalName); // 上传文件 const uploadResult = await this.storageProvider.upload(file.buffer, { filename, contentType: file.mimetype, folder: input.options?.folder || 'uploads' }); // 处理图片(生成缩略图、提取元数据) let metadata: any = {}; let thumbnailUrl: string | undefined; let width: number | undefined; let height: number | undefined; if (this.isImageFile(file.mimetype)) { const imageInfo = await this.imageProcessor.getImageInfo(file.buffer); width = imageInfo.width; height = imageInfo.height; metadata = imageInfo.metadata; // 生成缩略图 const thumbnail = await this.imageProcessor.resize(file.buffer, { width: 300, height: 300, fit: 'cover' }); const thumbnailResult = await this.storageProvider.upload(thumbnail, { filename: `thumb_${filename}`, contentType: file.mimetype, folder: 'thumbnails' }); thumbnailUrl = thumbnailResult.url; } // 创建媒体记录 const media = await entityManager.create('Media', { filename, originalName: file.originalName, mimeType: file.mimetype, size: file.size, width, height, url: uploadResult.url, thumbnailUrl, category: this.categorizeFile(file.mimetype), folderId: input.folderId, uploadedById: user.id, metadata, alt: input.metadata?.alt || '', caption: input.metadata?.caption || '', description: input.metadata?.description || '' }); return { success: true, data: { media }, events: [{ type: 'media.uploaded', data: { mediaId: media.id, filename: media.filename, category: media.category, uploadedBy: user.id } }] }; } private async validateFile(file: any): Promise<ValidationResult> { const errors: string[] = []; // 检查文件大小(默认最大50MB) const maxSize = 50 * 1024 * 1024; if (file.size > maxSize) { errors.push(`File size exceeds maximum allowed size of ${maxSize / 1024 / 1024}MB`); } // 检查文件类型 const allowedTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'video/mp4', 'video/webm', 'video/ogg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ]; if (!allowedTypes.includes(file.mimetype)) { errors.push(`File type '${file.mimetype}' is not allowed`); } return { valid: errors.length === 0, errors, warnings: [] }; } private generateUniqueFilename(originalName: string): string { const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 8); const extension = originalName.split('.').pop(); return `${timestamp}_${random}.${extension}`; } private isImageFile(mimeType: string): boolean { return mimeType.startsWith('image/'); } private categorizeFile(mimeType: string): string { if (mimeType.startsWith('image/')) return 'IMAGE'; if (mimeType.startsWith('video/')) return 'VIDEO'; if (mimeType.startsWith('audio/')) return 'AUDIO'; if (mimeType === 'application/pdf' || mimeType.includes('document')) return 'DOCUMENT'; return 'OTHER'; } async validate(input: any): Promise<ValidationResult> { const errors: string[] = []; if (!input.action) { errors.push('action is required'); } if (input.action === 'upload' && !input.file) { errors.push('file is required for upload action'); } if (['update', 'delete', 'resize', 'optimize'].includes(input.action) && !input.mediaId) { errors.push('mediaId is required for this action'); } return { valid: errors.length === 0, errors, warnings: [] }; } getRequiredPermissions(): string[] { return ['media_access']; } getMetadata(): ActionHandlerMetadata { return { async: true, timeout: 60000, retryable: true, maxRetries: 2 }; } }

视图定义

内容管理视图

// 内容列表视图 export const ContentListView: IViewDefinition = { id: 'content-list', name: 'ContentList', displayName: '内容列表', type: 'table', entityType: 'Content', config: { columns: [ { name: 'title', displayName: '标题', type: 'string', sortable: true, searchable: true, width: 250, renderer: 'content-title-link' }, { name: 'contentType.displayName', displayName: '内容类型', type: 'string', sortable: true, filterable: true, width: 120 }, { name: 'status', displayName: '状态', type: 'enum', sortable: true, filterable: true, width: 100, renderer: 'content-status-badge' }, { name: 'author.name', displayName: '作者', type: 'string', sortable: true, filterable: true, width: 120 }, { name: 'publishedAt', displayName: '发布时间', type: 'datetime', sortable: true, width: 140, format: { dateStyle: 'short', timeStyle: 'short' } }, { name: 'viewCount', displayName: '浏览量', type: 'number', sortable: true, width: 80 }, { name: 'updatedAt', displayName: '更新时间', type: 'datetime', sortable: true, width: 140, format: { dateStyle: 'short', timeStyle: 'short' } } ], defaultSort: [{ field: 'updatedAt', direction: 'desc' }], pageSize: 25, enableSearch: true, enableFilters: true, enableExport: true, filters: [ { name: 'status', displayName: '状态', type: 'select', options: [ { label: '草稿', value: 'DRAFT' }, { label: '待审核', value: 'PENDING' }, { label: '已发布', value: 'PUBLISHED' }, { label: '已归档', value: 'ARCHIVED' } ] }, { name: 'contentType', displayName: '内容类型', type: 'select', dataSource: 'ContentType', labelField: 'displayName', valueField: 'id' }, { name: 'publishedAt', displayName: '发布日期', type: 'dateRange' } ], actions: [ { name: 'create', displayName: '新建内容', type: 'primary', icon: 'plus', handler: 'navigate-to-create' }, { name: 'bulk-publish', displayName: '批量发布', type: 'secondary', icon: 'publish', handler: 'bulk-publish-modal', requiresSelection: true } ], rowActions: [ { name: 'edit', displayName: '编辑', icon: 'edit', handler: 'navigate-to-edit' }, { name: 'preview', displayName: '预览', icon: 'eye', handler: 'preview-content' }, { name: 'publish', displayName: '发布', icon: 'publish', handler: 'publish-content', condition: { status: 'DRAFT' } }, { name: 'duplicate', displayName: '复制', icon: 'copy', handler: 'duplicate-content' } ] } }; // 内容编辑视图 export const ContentEditView: IViewDefinition = { id: 'content-edit', name: 'ContentEdit', displayName: '内容编辑', type: 'form', entityType: 'Content', config: { layout: { type: 'sections', sections: [ { title: '基本信息', columns: 2, fields: [ { name: 'title', displayName: '标题', type: 'string', required: true, placeholder: '请输入内容标题' }, { name: 'slug', displayName: 'URL标识', type: 'string', readonly: true, help: '根据标题自动生成' }, { name: 'status', displayName: '状态', type: 'select', options: [ { label: '草稿', value: 'DRAFT' }, { label: '待审核', value: 'PENDING' }, { label: '已发布', value: 'PUBLISHED' } ], required: true }, { name: 'visibility', displayName: '可见性', type: 'select', options: [ { label: '公开', value: 'PUBLIC' }, { label: '私有', value: 'PRIVATE' }, { label: '密码保护', value: 'PASSWORD' }, { label: '仅会员', value: 'MEMBERS_ONLY' } ], required: true } ] }, { title: '内容', columns: 1, fields: [ { name: 'excerpt', displayName: '摘要', type: 'textarea', rows: 3, placeholder: '请输入内容摘要' }, { name: 'content', displayName: '正文', type: 'rich-text', required: true, config: { toolbar: 'full', plugins: ['image', 'video', 'table', 'code'], height: 400 } } ] }, { title: '媒体', columns: 2, fields: [ { name: 'featuredImage', displayName: '特色图片', type: 'image', config: { accept: 'image/*', maxSize: '10MB', dimensions: { width: 1200, height: 630 } } }, { name: 'gallery', displayName: '图片集', type: 'image-gallery', config: { maxImages: 10, allowReorder: true } } ] }, { title: '分类和标签', columns: 2, fields: [ { name: 'categoryIds', displayName: '分类', type: 'multi-select', dataSource: 'Category', labelField: 'name', valueField: 'id', config: { hierarchical: true, maxSelection: 3 } }, { name: 'tags', displayName: '标签', type: 'tag-input', config: { allowCreate: true, maxTags: 10 } } ] }, { title: 'SEO设置', columns: 1, collapsible: true, fields: [ { name: 'seoTitle', displayName: 'SEO标题', type: 'string', maxLength: 60, help: '建议长度:50-60个字符' }, { name: 'seoDescription', displayName: 'SEO描述', type: 'textarea', rows: 3, maxLength: 160, help: '建议长度:120-160个字符' }, { name: 'seoKeywords', displayName: 'SEO关键词', type: 'string', placeholder: '用逗号分隔多个关键词' }, { name: 'socialImage', displayName: '社交媒体图片', type: 'image', config: { dimensions: { width: 1200, height: 630 } } } ] }, { title: '发布设置', columns: 2, collapsible: true, fields: [ { name: 'publishedAt', displayName: '发布时间', type: 'datetime', help: '留空则立即发布' }, { name: 'expiresAt', displayName: '过期时间', type: 'datetime', help: '留空则永不过期' }, { name: 'password', displayName: '访问密码', type: 'string', condition: { field: 'visibility', value: 'PASSWORD' } } ] } ] }, actions: [ { name: 'save-draft', displayName: '保存草稿', type: 'secondary', handler: 'save-as-draft' }, { name: 'preview', displayName: '预览', type: 'secondary', handler: 'preview-content' }, { name: 'publish', displayName: '发布', type: 'primary', handler: 'publish-content' } ], validation: { rules: [ { field: 'title', type: 'required', message: '标题不能为空' }, { field: 'content', type: 'required', message: '正文不能为空' }, { field: 'seoTitle', type: 'maxLength', value: 60, message: 'SEO标题不能超过60个字符' } ] } } }; // 媒体库视图 export const MediaLibraryView: IViewDefinition = { id: 'media-library', name: 'MediaLibrary', displayName: '媒体库', type: 'gallery', entityType: 'Media', config: { displayMode: 'grid', // grid, list, masonry itemSize: 'medium', // small, medium, large showMetadata: true, groupBy: 'category', filters: [ { name: 'category', displayName: '媒体类型', type: 'select', options: [ { label: '图片', value: 'IMAGE' }, { label: '视频', value: 'VIDEO' }, { label: '音频', value: 'AUDIO' }, { label: '文档', value: 'DOCUMENT' } ] }, { name: 'folder', displayName: '文件夹', type: 'tree-select', dataSource: 'MediaFolder', labelField: 'name', valueField: 'id' }, { name: 'uploadedAt', displayName: '上传日期', type: 'dateRange' } ], actions: [ { name: 'upload', displayName: '上传媒体', type: 'primary', icon: 'upload', handler: 'upload-media-modal' }, { name: 'create-folder', displayName: '新建文件夹', type: 'secondary', icon: 'folder-plus', handler: 'create-folder-modal' } ], itemActions: [ { name: 'edit', displayName: '编辑', icon: 'edit', handler: 'edit-media-modal' }, { name: 'copy-url', displayName: '复制链接', icon: 'link', handler: 'copy-media-url' }, { name: 'move', displayName: '移动', icon: 'move', handler: 'move-media-modal' }, { name: 'delete', displayName: '删除', icon: 'trash', handler: 'delete-media-confirm' } ] } };

下一步

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

Last updated on