Talaween/Payload-LFRs
GitHub: Talaween/Payload-LFRs
为 Payload CMS 3.x 提供点赞、收藏、评分和评论功能的开箱即用插件,支持细粒度权限控制、评论审核与媒体附件。
Stars: 1 | Forks: 0
# Payload LFRs 插件
一个功能全面的 [Payload CMS 3.x](https://payloadcms.com) 插件,为现有集合添加 **Likes**、**Favourites**、**Ratings** 和 **Reviews** (LFRs) 功能。
## 功能
- **Likes & Dislikes**:允许用户对文档进行 like 或 dislike。Dislike 与 like 互斥。
- **Favourites**:允许用户将文档保存到收藏夹。
- **Ratings**:添加可自定义的评分系统(例如,5星、10分制、半星)。
- **Reviews & Replies**:允许用户撰写评论,其他人可以进行回复。
- **Review Media**:用户可以在评论中附加图片或视频
- **Admin Moderation**:审核视图,用于批准或删除评论和回复
- **Extensible API**:Headless REST API,提供完全的前端灵活性
## 截图
以下是使用此插件在前端构建的示例:
### 交互(Likes、Dislikes、Favourites)

### 评分分布与提交

### 评论与嵌套回复

## 安装
```
npm install payload-lfrs
# 或
pnpm add payload-lfrs
# 或
yarn add payload-lfrs
```
## 示例项目
本仓库包含一个功能完整的 Next.js 示例项目,位于 `dev` 文件夹中。它演示了如何将插件集成到 Payload 配置中,以及如何在前端应用中使用提供的 React 组件。
要在本地运行示例项目:
```
git clone https://github.com/Talaween/Payload-LFRs.git
cd Payload-LFRs
pnpm install
pnpm dev
```
示例应用将在 `http://localhost:3000` 提供。
## 基本用法
将插件添加到您的 Payload 配置中。以下是展示所有可用配置选项的综合示例:
```
import { buildConfig } from 'payload'
import { payloadLFRs } from 'payload-lfrs'
export default buildConfig({
// ... your existing config
plugins: [
payloadLFRs({
collections: {
// Target collection slug
posts: {
likes: true, // Enable likes for authenticated users
dislikes: true, // Enable dislikes (mutually exclusive with likes)
favourites: true, // Enable favourites
ratings: true, // Enable ratings
reviews: true, // Enable reviews
replies: ['admin'], // Enable replies, but restrict to admin roles
readReviews: 'public', // Access control for reading reviews ('public', true, or roles array)
allowMultipleReviews: false, // Prevent users from submitting multiple reviews on the same doc
enableReviewRating: true, // Force users to choose a rating score when reviewing
},
},
// Configure global rating options
rating: {
max: 5, // Max rating scale value (default: 5)
step: 0.5, // Value increment steps (default: 1)
icon: 'star', // Icon identifier hint for frontend (default: 'star')
},
// Enable review media uploads (requires an existing upload-enabled collection)
reviewMedia: {
uploadCollection: 'media',
allowedMimeTypes: ['image/*'],
maxFiles: 5,
maxFileSize: 5 * 1024 * 1024, // 5MB
},
reviewModeration: true, // Require reviews to be approved before they are public (default: false)
adminControls: true, // Set to false to hide the Global Settings from the Admin UI
adminGroup: 'LFRs', // Navigation group name in the Admin panel (default: 'LFRs')
usersCollectionSlug: 'users', // Slug of your auth collection (default: 'users')
}),
],
})
```
## 配置
`payloadLFRs` 插件接受一个包含以下属性的配置对象:
### `collections` (必填)
一个集合 slug 映射,用于启用 LFRs 功能。对于每个集合,您可以启用特定功能并配置访问控制。
```
collections: {
posts: {
likes: true, // Enable likes for any authenticated user
dislikes: false, // Disabled
favourites: ['admin', 'subscriber'], // Only specific roles can favourite
ratings: true,
reviews: true,
readReviews: 'public', // Set who can read reviews
allowMultipleReviews: true, // Allow users to leave multiple reviews (default: false)
enableReviewRating: false, // Make review ratings optional for comment-style reviews (default: true)
replies: ['admin'], // Enable replies, but only admins can respond
}
}
```
### `readReviews`
用于查看评论和回复的访问控制。
与默认为 `true`(需要身份验证)的交互功能不同,它默认为 `'public'`,允许任何人(包括访客)阅读评论和回复。您可以将其限制为特定角色(例如 `['admin']`)、仅限登录用户(`true`),或者提供一个自定义函数。
_(默认值:`'public'`)_
### `allowMultipleReviews`
如果为 `true`,用户可以对同一个文档提交多条评论。如果为 `false`,则限制为只能提交一条评论,并且 UI 组件将显示“编辑评论”按钮而不是“撰写评论”。
_(默认值:`false`)_
### `enableReviewRating`
如果为 `false`,用户可以在不强制提供星级评分的情况下提交评论。这有效地将评论系统转变为标准的评论/留言系统。
_(默认值:`true`)_
#### 访问控制
对于每个功能(`likes`、`dislikes`、`favourites`、`ratings`、`reviews`、`replies`),您可以提供:
- `true`:任何经过身份验证的用户都可以使用该功能(如果省略了功能键但提到了该功能,则为默认值,具体取决于实现/类型默认值)。
- `false`:此集合禁用该功能。
- `string[]`:只有 `roles` 数组中至少包含其中一个角色才能使用该功能。例如,`replies: ['admin']` 将回复限制为仅限管理员。
- `Function`:一个自定义的异步函数,接收请求和目标文档。返回 `true` 表示允许,返回 `false` 表示拒绝。
```
likes: async ({ req, targetCollection, targetDoc }) => {
// Custom logic: e.g., only users who purchased this product can review it
return true
}
```
### `rating`
配置评分系统(默认:5星制,整数)。
```
rating: {
max: 5, // Maximum rating value (default: 5)
step: 0.5, // Step increment, e.g., 0.5 for half-stars (default: 1)
icon: 'star', // Icon identifier hint for frontend (default: 'star')
}
```
### `reviewMedia`
允许用户在评论中附加媒体。**注意:** 您必须提供已启用上传功能的现有集合的 slug。
```
reviewMedia: {
uploadCollection: 'media', // REQUIRED: an existing upload collection in your payload config
allowedMimeTypes: ['image/jpeg', 'image/png'], // default: ['image/*']
maxFiles: 3, // default: 5
maxFileSize: 5 * 1024 * 1024, // 5MB limit
}
```
### `reviewModeration`
设置为 `true` 则要求评论在公开可见之前必须获得批准(默认值:`false`)。这还将在管理面板中添加一个专门的评论审核视图。
```
reviewModeration: true
```
### `usersCollectionSlug`
用于身份验证的用户集合的 slug(默认值:`'users'`)。
### `adminGroup`
LFRs 集合在 Admin UI 中显示的组名称(默认值:`'LFRs'`)。
### `adminControls`
设置为 `false` 可从 Payload Admin 面板中隐藏动态全局设置页面(`LFRs Settings`)(默认值:`true`)。这可以防止管理员在 runtime 动态覆盖插件的配置,同时保持对交互集合的访问。
### `disabled`
设置为 `true` 可完全禁用插件的功能,而无需卸载它或丢失数据(默认值:`false`)。
当 `disabled: true` 时,插件将继续注册其集合和字段以保持数据库 schema 的一致性(这对于 migrations 很重要),但它 _不会_ 注册任何 API endpoints、lifecycle hooks 或 Admin UI 组件。这非常适合在保持历史数据完整的同时暂时暂停交互。
### `collectionSlugs`
覆盖插件创建的内部集合(`likes`、`dislikes`、`favourites`、`ratings`、`reviews`、`replies`)的默认 slug。
### Admin UI Runtime 控制 (`LfrsSettings`)
插件会自动在管理面板中生成一个名为 `LFRs Settings` 的 **Payload Global**。这允许管理员在不更改代码或重启服务器的情况下,动态临时启用/禁用功能。
**重要提示:** 管理控制严格根据开发者的静态配置(`payload.config.ts`)生成。
- 如果开发者在代码中明确将某个功能(如 `Reviews`)设置为 `false`,管理员 **无法** 将其开启。
- 管理员覆盖操作(例如,在受到垃圾信息攻击时关闭审核或禁用 Likes)会立即与前端的 UI 同步,并且 REST API 会安全地阻止所有相关的 mutations 操作。
### `callbacks`
Hook 到用户交互和审核状态更改中,以触发自定义业务逻辑(例如,发送电子邮件通知、奖励积分、与外部系统同步)。所有 callbacks 都可以是异步的。
```
plugins: [
payloadLFRs({
// ...
callbacks: {
onReviewSubmitted: async ({ req, review }) => {
// Triggered when a user creates or edits a review
console.log(`Review submitted by user ${req.user.id}`)
},
onReviewStateChanged: async ({ req, review, previousStatus }) => {
// Triggered when an admin changes a review's moderation status
if (review.status === 'approved' && previousStatus !== 'approved') {
// Send a notification email to the author
}
},
onLiked: async ({ req, like }) => {
// Example: Award points to the target document's author
},
},
}),
]
```
**可用的 Callbacks(均接收 `PayloadRequest` 对象以及相关上下文):**
- **`onReviewSubmitted`**:`{ req, review }` — 在用户创建或更新评论时触发。
- **`onReviewDeleted`**:`{ req, reviewId, targetCollection, targetDoc }` — 在用户删除其评论时触发。
- **`onReplySubmitted`**:`{ req, reply }` — 在用户创建或更新回复时触发。
- **`onReviewStateChanged`**:`{ req, review, previousStatus }` — 在管理员通过 Admin 面板更改评论的 `status` 时触发。
- **`onRatingSubmitted`**:`{ req, rating }` — 在用户提交全新的独立评分时触发。
- **`onRatingUpdated`**:`{ req, rating }` — 在用户更新其现有评分时触发。
- **`onLiked`**:`{ req, like }` — 在用户 like 一篇文档时触发。
- **`onUnliked`**:`{ req, targetCollection, targetDoc }` — 在用户移除其 like 时触发。
- **`onDisliked`**:`{ req, dislike }` — 在用户 dislike 一篇文档时触发。
- **`onUndisliked`**:`{ req, targetCollection, targetDoc }` — 在用户移除其 dislike 时触发。
## 工作原理
1. **添加 Collections**:插件自动创建集合以存储交互(例如 `lfrs_likes`、`lfrs_reviews`)。
2. **注入 Fields**:它将一个 `lfrs` 字段组注入到您的目标集合中,其中包含聚合数据(例如 `lfrs.likesCount`、`lfrs.averageRating`)。
3. **创建 Endpoints**:它在 `/api/lfrs/...` 下注册 REST endpoints 以处理交互(例如 `/api/lfrs/like`、`/api/lfrs/rate`)。
4. **Admin UI**:将自定义组件和审核视图添加到 Payload Admin 面板。
### 交互状态小部件
对于启用了 LFRs 功能的每个目标集合,插件会将一个自定义的 **交互状态小部件** 注入到文档的编辑视图侧边栏中。此小部件显示该文档所有聚合交互的快速摘要(例如总 likes 数、总评论数和平均评分)。
### 评论审核视图
如果在您的配置中启用了 `reviewModeration: true`,插件将在管理面板中提供一个专门的 **评论审核队列** 视图。通过 `/admin/lfrs-moderation` 访问,此仪表板允许管理员在待处理用户评论和回复公开展示之前,对其进行高效的审查、批准或拒绝。
## API Endpoints
插件公开了多个 endpoints,供您的前端与 LFRs 功能进行交互:
- `POST /api/lfrs/like` — 切换用户对文档的 like 状态。
- **Body:** `{ collection: string, id: string }`
- **返回:** `{ liked: boolean, likesCount: number, disliked?: boolean, dislikesCount?: number }`
- `POST /api/lfrs/dislike` — 切换用户对文档的 dislike 状态。
- **Body:** `{ collection: string, id: string }`
- **返回:** `{ disliked: boolean, dislikesCount: number, liked?: boolean, likesCount?: number }`
- `POST /api/lfrs/favourite` — 切换用户对文档的收藏状态。
- **Body:** `{ collection: string, id: string }`
- **返回:** `{ favourited: boolean, favouritesCount: number }`
- `POST /api/lfrs/rate` — 提交或更新用户的评分。
- **Body:** `{ collection: string, id: string, score: number }`
- **返回:** `{ rating: object, ratingConfig: object, ratingsAverage: number, ratingsCount: number }`
- `POST /api/lfrs/review` — 提交或更新用户评论。
- **Body:** `{ collection: string, id: string, body: string, title?: string, score?: number, media?: string[], reviewId?: string }`
- **返回:** `{ review: object, reviewsCount: number }`
- `DELETE /api/lfrs/review` — 删除用户的评论。
- **Body:** `{ reviewId: string }`
- **返回:** `{ deleted: true, reviewsCount: number }`
- `POST /api/lfrs/reply` — 提交或更新对评论的回复。
- **Body:** `{ body: string, reviewId: string, replyId?: string }`
- **返回:** `{ reply: object, repliesCount: number }`
- `DELETE /api/lfrs/reply` — 删除回复。
- **Body:** `{ replyId: string }`
- **返回:** `{ deleted: true, repliesCount: number }`
- `GET /api/lfrs/status` — 返回功能配置标志和当前用户的交互状态。
- **Query:** `collection` (必填), `id` (必填)
- **返回:** `{ likesCount: number, dislikesCount: number, liked: boolean, favourited: boolean, rating: number | null, review: object | null, ... }`
- `GET /api/lfrs/interactions` — 获取分页的评分或评论列表。
- **Query:** `collection` (必填), `id` (必填), `type` (可选:`'reviews'` | `'ratings'`,默认为 `'reviews'`), `page` (可选), `limit` (可选), `sort` (可选:`'newest'` | `'oldest'` | `'highest'` | `'lowest'`)
- **返回:** `{ docs: Array, page: number, totalDocs: number, totalPages: number }`
- `GET /api/lfrs/distribution` — 返回评分的分布频率。
- **Query:** `collection` (必填), `id` (必填)
- **返回:** `{ averageScore: number, totalRatings: number, distribution: Array<{ score: number, count: number, percentage: number }> }`
- `GET /api/lfrs/user-favourites` — 返回用户收藏的文档 ID。
- **Query:** `collection` (必填), `userId` (必填), `limit` (可选)
- **返回:** `{ ids: string[] }`
- `GET /api/lfrs/user-reviews` — 获取特定用户对某文档的评论。
- **Query:** `collection` (必填), `id` (必填), `userId` (必填)
- **返回:** `{ reviews: Array }`
- `GET /api/lfrs/likes-count` — 获取总 likes 数。
- **Query:** `collection` (必填), `id` (必填)
- **返回:** `{ likesCount: }`
- `GET /api/lfrs/dislikes-count` — 获取总 dislikes 数。
- **Query:** `collection` (必填), `id` (必填)
- **返回:** `{ dislikesCount: number }`
- `GET /api/lfrs/likes-users` — 获取 like 了某文档的用户 ID。
- **Query:** `collection` (必填), `id` (必填), `limit` (可选)
- **返回:** `{ userIds: string[] }`
- `GET /api/lfrs/dislikes-users` — 获取 dislike 了某文档的用户 ID。
- **Query:** `collection` (必填), `id` (必填), `limit` (可选)
- **返回:** `{ userIds: string[] }`
- **所有 `POST` 和 `DELETE` endpoints 都需要身份验证(带有用户上下文)。**
## 前端 UI 组件
该插件为您的前端应用程序提供了一套现成的 React 组件。这些组件通过 `payload-lfrs/client` 导出,并作为客户端组件(`"use client"`)构建,以无缝处理用户交互和乐观 UI 更新。
### 可用组件
- **`LfrsLikeDislike`**:一个可切换的赞/踩小部件,显示当前计数。
- **`LfrsFavourite`**:一个用于保存文档的书签/收藏按钮。
- **`LfrsRating`**:一个交互式星级评分组件,供用户提交评分。
- **`LfrsRatingSummary`**:一个显示平均评分和分数分布的视觉摘要。
- **`LfrsComposeReview` / `LfrsComposeReply`**:用于提交文本评论和嵌套回复的表单。
- **`LfrsReviewCard` / `LfrsReplyCard`**:用于渲染单个评论和回复的展示组件。
- **`LfrsReviewsSection`**:一个完整、集成的评论区,结合了摘要、撰写表单和评论列表。
### 示例用法
```
import { LfrsLikeDislike, LfrsRating } from 'payload-lfrs/client'
export function PostDetails({ post }) {
return (
{/* 5-Star Rating */}
)
}
```
### UI 自定义 (CSS 变量)
UI 组件旨在与您现有的网站无缝集成。它们使用 **CSS 变量**,您可以在应用中全局覆盖它们(例如在您的 `:root` 或 `body` 块中),或者使用 `style` prop 直接作用到组件上!
以下是可用的变量及其默认回退值:
```
:root {
--lfrs-primary: #000000; /* Used for primary buttons (e.g. "Write a Review") */
--lfrs-text: #333333; /* Main text color */
--lfrs-text-muted: #666666; /* Dates, secondary text, placeholders */
--lfrs-bg: #ffffff; /* Main background color for cards */
--lfrs-bg-muted: #f5f5f5; /* Background for replies and forms */
--lfrs-border: #e0e0e0; /* Borders around cards and inputs */
--lfrs-star-active: #ffb400; /* Color of filled rating stars and bars */
--lfrs-star-inactive: #e0e0e0; /* Color of empty rating stars */
--lfrs-like-active: #0066cc; /* Active state for Like button */
--lfrs-dislike-active: #cc0000; /* Active state for Dislike button */
--lfrs-favourite-active: #ff0055; /* Active state for Favourite button */
--lfrs-radius: 6px; /* Border radius for buttons, inputs, and cards */
--lfrs-font: inherit; /* Font family inherited from your app by default */
}
```
#### 示例:内联作用域自定义
如果您想为单个组件设置样式,可以通过 `style` prop 传递变量:
```
```
## 构建自定义 UI(Headless 用法)
该插件被设计为完全与框架无关。虽然为了方便我们提供了 React 组件,但您可以通过直接与插件的 REST API 交互,在任何框架(Vue、Svelte、Angular、React Native 或原生 JavaScript)中构建您自己的自定义用户界面。
### 自定义组件示例
以下是如何在原生 JavaScript 中构建自定义“Like”交互的示例:
```
async function toggleLike(targetCollection, targetDocId) {
try {
const response = await fetch('/api/lfrs/like', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 'Authorization': 'Bearer YOUR_TOKEN' // If required
},
body: JSON.stringify({
collection: targetCollection,
id: targetDocId,
}),
})
if (!response.ok) throw new Error('Failed to toggle like')
const data = await response.json()
// Update your custom UI state here...
} catch (error) {
console.error(error)
}
}
```
同样,您可以使用 `GET /api/lfrs/status` endpoint 在页面加载时获取当前用户的交互状态,并将其他交互(Favorites、Ratings、Reviews)映射到它们各自的 endpoints。
## 架构与开发者指南
如果您正在审查、贡献或调试此插件,以下是代码库结构和内部架构的概述。
### 代码组织
- `src/plugin.ts`:主入口点。它接收用户配置,对其进行清理(应用默认值),并将集合、字段和 endpoints 注入到 Payload 配置中。
- `src/collections/`:包含插件管理的集合定义(`likes`、`dislikes`、`favourites`、`ratings`、`reviews`、`replies`)。它们存储了实际的用户交互。
- `src/fields/`:
- `aggregateFields.ts`:生成注入到目标集合中的 `lfrs` 字段组(例如 `lfrs.likesCount`、`lfrs.averageRating`)。
- `joinFields.ts`:注入 Payload Join 字段,以便管理员可以直接从目标文档的 admin UI 中查看相关的 LFRs 文档。
- `src/endpoints/`:REST API 实现。它们处理传入的用户请求,执行访问控制,并执行数据库操作。
- `src/hooks/`:包含 Payload 的 lifecycle hooks。例如,`cascadeDelete.ts` 确保当目标文档被删除时,所有相关的交互也会被移除,以防止产生孤儿记录。
- `src/admin/`:用于 Payload Admin 面板的 React 组件。包括状态小部件和评论审核视图。
- `src/types.ts`:用于配置、内部清理配置和功能访问的 TypeScript 接口和类型。
### 聚合计数逻辑 (Endpoint-Driven)
为了确保高可靠性并避免 Payload CMS 内出现事务上下文污染,聚合逻辑(例如更新文章的 `likesCount` 或 `dislikesCount`)主要采用 **Endpoint-Driven** 方式:
1. **Endpoints 抑制 Hooks**:当用户通过 API endpoints 进行交互时(例如 `/api/lfrs/like`),endpoints 会执行必要的数据库 mutations(`create`、`delete`),同时传递 `context: { skipLfrsHooks: true }`。这会抑制自动的基于 hook 的重新计算。
2. **显式更新**:所有 mutations 成功完成后,endpoint 会直接从数据库(作为真实数据源)显式统计交互,并对目标文档的聚合字段执行一次原子更新。
3. **Admin 面板回退**:`src/hooks/recalculateAggregates.ts` 中的 `afterChange` 和 `afterDelete` hooks 仍作为回退方案保留。如果管理员从 Payload Admin UI 手动创建或删除交互,它们将自动重新计算计数,从而保持数据的一致性。
## License
MIT
{post.title}
{/* Like / Dislike Toggle */}标签:CMS插件, Payload, Syscall, TypeScript, Web开发, 互动功能, 安全插件, 自动化攻击, 评论系统