diff --git a/ghost/core/core/server/api/endpoints/posts.js b/ghost/core/core/server/api/endpoints/posts.js index 1732e9c..b8f3f84 100644 --- a/ghost/core/core/server/api/endpoints/posts.js +++ b/ghost/core/core/server/api/endpoints/posts.js @@ -1,7 +1,7 @@ const urlUtils = require('../../../shared/url-utils'); const models = require('../../models'); const getPostServiceInstance = require('../../services/posts/posts-service'); -const allowedIncludes = [ +const allowedIncludes = [ //允许包含的关联数据 'tags', 'authors', 'authors.roles', @@ -18,12 +18,13 @@ const allowedIncludes = [ 'post_revisions', 'post_revisions.author' ]; -const unsafeAttrs = ['status', 'authors', 'visibility']; +const unsafeAttrs = ['status', 'authors', 'visibility']; //不安全属性列表 -const postsService = getPostServiceInstance(); +const postsService = getPostServiceInstance(); //文章服务实例 /** * @param {string} event + * 根据文章状态变更事件生成缓存失效头信息 */ function getCacheHeaderFromEventString(event, dto) { if (event === 'published_updated' || event === 'unpublished') { @@ -44,76 +45,98 @@ function getCacheHeaderFromEventString(event, dto) { } } -/** @type {import('@tryghost/api-framework').Controller} */ +/** + * Ghost CMS 文章 API 控制器 + * 提供对文章的各种操作的api 控制器 + * 这个控制器实现了完整的文章 CRUD(创建、读取、更新、删除)操作, + * 以及批量操作、数据导出等高级功能。它遵循 Ghost API 框架规范, + * 每个端点都包含完整的配置:权限控制、参数验证、缓存管理等。 + * + * @type {import('@tryghost/api-framework').Controller} + */ const controller = { + // 控制器文档名称,用于 API 文档生成 docName: 'posts', + + /** + * 获取文章列表端点 + * 支持分页、过滤、排序、字段选择等高级查询功能 + */ browse: { headers: { - cacheInvalidate: false + cacheInvalidate: false // 列表查询不缓存失效 }, options: [ - 'include', - 'filter', - 'fields', - 'collection', - 'formats', - 'limit', - 'order', - 'page', - 'debug', - 'absolute_urls' + 'include', // 包含关联数据(标签、作者等) + 'filter', // 过滤条件 + 'fields', // 选择返回字段 + 'collection', // 集合过滤 + 'formats', // 内容格式 + 'limit', // 分页大小 + 'order', // 排序方式 + 'page', // 页码 + 'debug', // 调试模式 + 'absolute_urls' // 绝对URL ], validation: { options: { include: { - values: allowedIncludes + values: allowedIncludes // 只允许预定义的关联数据 }, formats: { - values: models.Post.allowedFormats + values: models.Post.allowedFormats // 只允许支持的格式 } } }, permissions: { - unsafeAttrs: unsafeAttrs + unsafeAttrs: unsafeAttrs // 限制不安全属性修改 }, query(frame) { - return postsService.browsePosts(frame.options); + return postsService.browsePosts(frame.options); // 调用服务层 } }, + /** + * 导出文章分析数据为 CSV 格式 + * 用于数据分析和报表生成 + */ exportCSV: { options: [ - 'limit', - 'filter', - 'order' + 'limit', // 导出数量限制 + 'filter', // 过滤条件 + 'order' // 排序方式 ], headers: { disposition: { - type: 'csv', + type: 'csv', // 文件类型 value() { const datetime = (new Date()).toJSON().substring(0, 10); - return `post-analytics.${datetime}.csv`; + return `post-analytics.${datetime}.csv`; // 带时间戳的文件名 } }, - cacheInvalidate: false + cacheInvalidate: false // 导出操作不缓存失效 }, response: { - format: 'plain' + format: 'plain' // 纯文本响应格式 }, permissions: { - method: 'browse' + method: 'browse' // 复用浏览权限 }, validation: {}, async query(frame) { return { - data: await postsService.export(frame) + data: await postsService.export(frame) // 调用导出服务 }; } }, + /** + * 获取单篇文章详情 + * 支持通过 ID、slug 或 UUID 查询 + */ read: { headers: { - cacheInvalidate: false + cacheInvalidate: false // 单篇文章查询不缓存失效 }, options: [ 'include', @@ -121,14 +144,14 @@ const controller = { 'formats', 'debug', 'absolute_urls', - // NOTE: only for internal context - 'forUpdate', - 'transacting' + // 内部上下文专用选项 + 'forUpdate', // 用于更新操作 + 'transacting' // 事务处理 ], data: [ - 'id', - 'slug', - 'uuid' + 'id', // 文章ID + 'slug', // 文章别名 + 'uuid' // 全局唯一标识 ], validation: { options: { @@ -144,19 +167,23 @@ const controller = { unsafeAttrs: unsafeAttrs }, query(frame) { - return postsService.readPost(frame); + return postsService.readPost(frame); // 调用读取服务 } }, + /** + * 创建新文章 + * 状态码 201 表示创建成功 + */ add: { - statusCode: 201, + statusCode: 201, // 创建成功状态码 headers: { - cacheInvalidate: false + cacheInvalidate: false // 默认不缓存失效 }, options: [ 'include', 'formats', - 'source' + 'source' // 内容来源(HTML) ], validation: { options: { @@ -164,7 +191,7 @@ const controller = { values: allowedIncludes }, source: { - values: ['html'] + values: ['html'] // 只支持HTML源 } } }, @@ -173,6 +200,7 @@ const controller = { }, async query(frame) { const model = await models.Post.add(frame.data.posts[0], frame.options); + // 如果文章状态为已发布,则失效所有缓存 if (model.get('status') === 'published') { frame.setHeader('X-Cache-Invalidate', '/*'); } @@ -181,22 +209,26 @@ const controller = { } }, + /** + * 编辑文章 + * 支持智能缓存失效和事件处理 + */ edit: { headers: { /** @type {boolean | {value: string}} */ - cacheInvalidate: false + cacheInvalidate: false // 初始不缓存失效 }, options: [ 'include', - 'id', + 'id', // 必须提供文章ID 'formats', 'source', - 'email_segment', - 'newsletter', - 'force_rerender', - 'save_revision', - 'convert_to_lexical', - // NOTE: only for internal context + 'email_segment', // 邮件分段 + 'newsletter', // 新闻稿设置 + 'force_rerender', // 强制重新渲染 + 'save_revision', // 保存修订版本 + 'convert_to_lexical', // 转换为Lexical格式 + // 内部上下文专用选项 'forUpdate', 'transacting' ], @@ -206,7 +238,7 @@ const controller = { values: allowedIncludes }, id: { - required: true + required: true // ID为必填项 }, source: { values: ['html'] @@ -218,12 +250,13 @@ const controller = { }, async query(frame) { let model = await postsService.editPost(frame, { + // 事件处理器,根据文章状态变更智能处理缓存 eventHandler: (event, dto) => { const cacheInvalidate = getCacheHeaderFromEventString(event, dto); if (cacheInvalidate === true) { - frame.setHeader('X-Cache-Invalidate', '/*'); + frame.setHeader('X-Cache-Invalidate', '/*'); // 失效所有缓存 } else if (cacheInvalidate?.value) { - frame.setHeader('X-Cache-Invalidate', cacheInvalidate.value); + frame.setHeader('X-Cache-Invalidate', cacheInvalidate.value); // 失效特定URL缓存 } } }); @@ -232,62 +265,74 @@ const controller = { } }, + /** + * 批量编辑文章 + * 基于过滤条件对多篇文章执行相同操作 + */ bulkEdit: { - statusCode: 200, + statusCode: 200, // 操作成功状态码 headers: { - cacheInvalidate: true + cacheInvalidate: true // 批量操作需要缓存失效 }, options: [ - 'filter' + 'filter' // 必须提供过滤条件 ], data: [ - 'action', - 'meta' + 'action', // 操作类型(必填) + 'meta' // 操作元数据 ], validation: { data: { action: { - required: true + required: true // 操作类型为必填项 } }, options: { filter: { - required: true + required: true // 过滤条件为必填项 } } }, permissions: { - method: 'edit' + method: 'edit' // 复用编辑权限 }, async query(frame) { return await postsService.bulkEdit(frame.data.bulk, frame.options); } }, + /** + * 批量删除文章 + * 基于过滤条件删除多篇文章 + */ bulkDestroy: { statusCode: 200, headers: { - cacheInvalidate: true + cacheInvalidate: true // 删除操作需要缓存失效 }, options: [ - 'filter' + 'filter' // 必须提供过滤条件 ], permissions: { - method: 'destroy' + method: 'destroy' // 复用删除权限 }, async query(frame) { return await postsService.bulkDestroy(frame.options); } }, + /** + * 删除单篇文章 + * 状态码 204 表示无内容返回(删除成功) + */ destroy: { - statusCode: 204, + statusCode: 204, // 删除成功状态码(无内容) headers: { - cacheInvalidate: true + cacheInvalidate: true // 删除操作需要缓存失效 }, options: [ 'include', - 'id' + 'id' // 必须提供文章ID ], validation: { options: { @@ -295,7 +340,7 @@ const controller = { values: allowedIncludes }, id: { - required: true + required: true // ID为必填项 } } }, @@ -303,34 +348,39 @@ const controller = { unsafeAttrs: unsafeAttrs }, query(frame) { - return models.Post.destroy({...frame.options, require: true}); + return models.Post.destroy({...frame.options, require: true}); // 直接调用模型层 } }, + /** + * 复制文章 + * 创建文章的副本,保留原文章内容但生成新的标识 + */ copy: { - statusCode: 201, + statusCode: 201, // 创建成功状态码 headers: { location: { + // 生成复制后文章的位置URL resolve: postsService.generateCopiedPostLocationFromUrl }, - cacheInvalidate: false + cacheInvalidate: false // 复制操作不缓存失效 }, options: [ - 'id', - 'formats' + 'id', // 必须提供原文章ID + 'formats' // 内容格式 ], validation: { id: { - required: true + required: true // ID为必填项 } }, permissions: { - method: 'add' + method: 'add' // 复用添加权限 }, async query(frame) { - return postsService.copyPost(frame); + return postsService.copyPost(frame); // 调用复制服务 } } }; -module.exports = controller; +module.exports = controller; \ No newline at end of file diff --git a/ghost/core/core/server/services/posts/posts-service.js b/ghost/core/core/server/services/posts/posts-service.js index b9d1642..32eff4f 100644 --- a/ghost/core/core/server/services/posts/posts-service.js +++ b/ghost/core/core/server/services/posts/posts-service.js @@ -1,47 +1,102 @@ -const PostsService = require('./PostsService'); -const PostsExporter = require('./PostsExporter'); -const url = require('../../../server/api/endpoints/utils/serializers/output/utils/url'); +/** + * Ghost CMS 文章服务工厂函数 + * + * 这个模块负责创建和配置文章服务的单例实例,采用工厂模式和依赖注入设计, + * 将各种依赖组件组装成一个完整的文章服务。它确保了服务实例的一致性和可测试性。 + */ + +// 导入核心服务组件 +const PostsService = require('./PostsService'); // 主文章业务逻辑服务 +const PostsExporter = require('./PostsExporter'); // 文章导出功能组件 +const url = require('../../../server/api/endpoints/utils/serializers/output/utils/url'); // URL序列化工具 /** - * @returns {InstanceType} instance of the PostsService + * 文章服务工厂函数 + * + * 这个函数采用延迟加载模式,在需要时才加载依赖模块,避免循环依赖问题。 + * 它负责组装文章服务所需的所有依赖组件,并返回配置完整的服务实例。 + * + * @returns {InstanceType} 配置完整的文章服务实例 */ const getPostServiceInstance = () => { - const urlUtils = require('../../../shared/url-utils'); - const labs = require('../../../shared/labs'); - const models = require('../../models'); - const PostStats = require('./stats/PostStats'); - const emailService = require('../email-service'); - const settingsCache = require('../../../shared/settings-cache'); - const settingsHelpers = require('../settings-helpers'); + // 延迟加载依赖模块(避免循环依赖) + const urlUtils = require('../../../shared/url-utils'); // URL处理工具 + const labs = require('../../../shared/labs'); // 功能开关管理(A/B测试、实验性功能) + const models = require('../../models'); // 数据库模型层 + const PostStats = require('./stats/PostStats'); // 文章统计服务 + const emailService = require('../email-service'); // 邮件服务 + const settingsCache = require('../../../shared/settings-cache'); // 设置缓存 + const settingsHelpers = require('../settings-helpers'); // 设置辅助工具 + // 实例化文章统计服务 const postStats = new PostStats(); + /** + * 配置文章导出器实例 + * + * 导出器负责将文章数据转换为CSV等格式,支持数据分析需求。 + * 配置包括数据库模型映射、URL生成函数和设置相关依赖。 + */ const postsExporter = new PostsExporter({ + // 数据库模型映射 models: { - Post: models.Post, - Newsletter: models.Newsletter, - Label: models.Label, - Product: models.Product + Post: models.Post, // 文章模型 + Newsletter: models.Newsletter, // 新闻稿模型 + Label: models.Label, // 标签模型 + Product: models.Product // 产品模型 }, + /** + * 文章URL生成函数 + * + * 为导出功能提供文章的标准URL,确保导出的数据包含正确的链接信息。 + * + * @param {Object} post - 文章对象 + * @returns {string} 文章的完整URL + */ getPostUrl(post) { - const jsonModel = post.toJSON(); + const jsonModel = post.toJSON(); // 转换为JSON格式 + // 使用URL序列化工具生成文章URL url.forPost(post.id, jsonModel, {options: {}}); - return jsonModel.url; + return jsonModel.url; // 返回生成的URL }, - settingsCache, - settingsHelpers + settingsCache, // 设置缓存依赖 + settingsHelpers // 设置辅助工具依赖 }); + /** + * 创建并返回配置完整的文章服务实例 + * + * 采用依赖注入模式,将所有必要的依赖通过构造函数注入, + * 提高了代码的可测试性和可维护性。 + */ return new PostsService({ - urlUtils: urlUtils, - models: models, + urlUtils: urlUtils, // URL处理工具 + models: models, // 数据库模型层 + /** + * 功能开关检查函数 + * + * 用于检查特定功能是否启用,支持A/B测试和实验性功能。 + * 注意:使用箭头函数而非bind,以保持测试时的可替换性。 + * + * @param {string} flag - 功能标识符 + * @returns {boolean} 功能是否启用 + */ isSet: flag => labs.isSet(flag), // don't use bind, that breaks test subbing of labs - stats: postStats, - emailService: emailService.service, - postsExporter + stats: postStats, // 文章统计服务 + emailService: emailService.service, // 邮件服务(取service属性) + postsExporter // 文章导出器 }); }; +// 导出工厂函数作为模块的主要接口 module.exports = getPostServiceInstance; + +/** + * 暴露PostsService类(仅用于测试目的) + * + * 这个导出项允许测试代码直接访问PostsService类, + * 便于进行单元测试和集成测试。 + * 在生产环境中应该通过工厂函数获取服务实例。 + */ // exposed for testing purposes only -module.exports.PostsService = PostsService; +module.exports.PostsService = PostsService; \ No newline at end of file