通过使用 Prisma 和 GraphQL 实现文章点赞功能,可以学习如何设计高效的数据库模型、处理简单的关联关系(如文章与点赞的一对多关系),以及如何在 GraphQL 中处理数据查询与变更。
1. 点赞逻辑
- 一篇文章可以多个用户按赞
- 每个用户同一篇文章只能按赞一次,已赞的再次点击取消按赞
2. 设计点赞模型
prisma.schema
新增 Like
模型,同步数据库 npx prisma migrate dev --name add_like
ts
model Like {
id String @id @default(cuid())
postId String
post Post @relation(fields: [postId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User @relation(fields: [createdById], references: [id])
createdById String
@@unique([createdById, postId]) // 确保每个用户对每个帖子只能点赞一次
}
3. Graphql 增加点赞接口
增加 likePost
unlikePost
接口,
ts
// graphql/types/Like.ts
import { builder } from '../builder'
builder.prismaObject('Like', {
fields: (t) => ({
id: t.exposeID('id'),
postId: t.exposeID('postId'),
post: t.relation('post'),
createdAt: t.expose('createdAt', { type: 'DateTime' }),
updatedAt: t.expose('updatedAt', { type: 'DateTime' }),
createdById: t.exposeID('createdById'),
createdBy: t.relation('createdBy'),
}),
})
builder.mutationType({
fields: (t) => ({
likePost: t.field({
type: 'Like' as any,
args: {
postId: t.arg.id(),
userId: t.arg.id(),
},
resolve: async (_root: any, args: any, ctx: any) => {
const { postId, userId } = args
const like = await ctx.prisma.like.create({
data: {
postId,
createdById: userId,
},
})
return like
},
}),
unlikePost: t.field({
type: 'Like' as any,
args: {
postId: t.arg.id(),
userId: t.arg.id(),
},
resolve: async (_root: any, args: any, ctx: any) => {
const { postId, userId } = args
const like = await ctx.prisma.like.delete({
where: {
createdById_postId: {
createdById: userId,
postId,
},
},
})
return like
},
}),
}),
})
4. 文章接口拿到点赞数据
likeCount
:统计文章点赞数量isLikedByUser
:判断当前用户是否已经按赞- 删除文章,把下面的赞一并删除
ts
// graphql/types/Post.ts
import prisma from '@/lib/prisma'
import { builder } from '../builder'
builder.prismaObject('Post', {
fields: (t: any) => ({
//...
likes: t.relation('likes'),
likeCount: t.int({
resolve: async (parent: any, _args: any, ctx: any) => {
const likeCount = await ctx.prisma.like.count({
where: {
postId: parent.id,
},
})
return likeCount
},
}),
isLikedByUser: t.boolean({
args: {
userId: t.arg.id(),
},
resolve: async (parent: any, _args: any, ctx: any) => {
const { userId } = _args
if (!userId) {
return false
}
const like = await ctx.prisma.like.findUnique({
where: {
createdById_postId: {
createdById: userId,
postId: parent.id,
},
},
})
return like !== null
},
}),
}),
})
// 定义添加链接的类型
builder.mutationType({
fields: (t: any) => ({
deletePost: t.field({
type: 'Post' as any,
args: {
id: t.arg.id(),
},
resolve: async (_root: any, args: any, ctx: any) => {
const { id } = args
// 删除与该 Post 相关的 Like 记录
await ctx.prisma.like.deleteMany({
where: { postId: id },
})
// 然后删除 Post
await ctx.prisma.post.delete({
where: { id },
})
return true
},
}),
}),
} as any)
5. 前端点赞组件
tsx
// LikeAction.tsx
'use client'
import { Button } from '@/components/ui/button'
import { gql, useMutation } from '@apollo/client'
import { Heart } from 'lucide-react'
import { useLocale } from 'next-intl'
import { useState } from 'react'
import { toast } from 'sonner'
type Props = {
post: any
currentUserId?: string
}
const LikeAction = ({ post, currentUserId }: Props) => {
const locale = useLocale()
const [isLiked, setIsLiked] = useState<boolean>(post.isLikedByUser)
const [likeCount, setLikeCount] = useState<number>(post.likeCount)
const [likePost, { loading: likingPost }] = useMutation(
gql`
mutation LikePost($postId: ID!, $userId: ID!) {
likePost(postId: $postId, userId: $userId) {
id
}
}
`,
{
onCompleted: (data) => {
setIsLiked(true)
setLikeCount((prev) => prev + 1)
},
},
)
const [unlikePost, { loading: unlikingPost }] = useMutation(
gql`
mutation UnlikePost($postId: ID!, $userId: ID!) {
unlikePost(postId: $postId, userId: $userId) {
id
}
}
`,
{
onCompleted: (data) => {
setIsLiked(false)
setLikeCount((prev) => prev - 1)
},
},
)
const handleLikeToggle = () => {
if (!currentUserId) {
toast.info('Please login to like this post')
}
if (isLiked) {
unlikePost({
variables: { postId: post.id, userId: currentUserId },
})
} else {
likePost({
variables: { postId: post.id, userId: currentUserId },
})
}
}
return (
<Button
variant="ghost"
size="sm"
onClick={handleLikeToggle}
disabled={likingPost || unlikingPost}
className={`flex cursor-pointer items-center gap-1 ${isLiked ? 'text-red-500' : 'text-gray-500'}`}
>
<Heart className={`h-4 w-4 ${isLiked ? 'fill-current' : ''}`} />
<span>{likeCount}</span>
</Button>
)
}
export default LikeAction