全栈开发个人博客11.文章点赞设计

通过使用 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
相关推荐
我这里是好的呀6 小时前
全栈开发个人博客12.嵌套评论设计
前端·全栈
我这里是好的呀6 小时前
全栈开发个人博客13.AI聊天设计
前端·全栈
我这里是好的呀6 小时前
全栈开发个人博客09.Authentication
全栈
susnm2 天前
创建你的第一个 Dioxus app
rust·全栈
coco01244 天前
云上教室选座系统开发实战:基于 Vue3、Express 与 MongoDB 的全栈实践
vue.js·全栈
我这里是好的呀4 天前
全栈开发个人博客08.前端接入Graphql
全栈
我这里是好的呀5 天前
全栈开发个人博客02:国际化
全栈
零道5 天前
我用了一周时间,复刻了一个Bolt new
ai编程·全栈·deepseek
Cyber4K5 天前
《48小时极速开发:Python+MySQL 学生信息管理系统架构实战揭秘》
python·全栈