pull/2/head
ws 3 months ago
parent d3fbccac3e
commit 99192e1ee1

@ -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) // 调用导出服务
};
}
},
/**
* 获取单篇文章详情
* 支持通过 IDslug 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;

@ -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<PostsService>} instance of the PostsService
* 文章服务工厂函数
*
* 这个函数采用延迟加载模式在需要时才加载依赖模块避免循环依赖问题
* 它负责组装文章服务所需的所有依赖组件并返回配置完整的服务实例
*
* @returns {InstanceType<PostsService>} 配置完整的文章服务实例
*/
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;
Loading…
Cancel
Save