Tube - Video Reactions

Schema
  • 列出reaction的枚举值
  • 明确视频、用户、videoReactions之间的逻辑关系,用户和视频都可以对应多个reactions,但一个reaction只能由一个用户产生且只作用于一个视频
  • 生成reaction变更时的数据校验格式
复制代码

export const reactionType = pgEnum('reaction_type', [ 'like', 'dislike' ])

复制代码

export const videoReactions = pgTable("video_reactions", { userId: uuid("user_id").references(() => users.id, { onDelete: "cascade", }).notNull(), videoId: uuid("video_id").references(() => videos.id, { onDelete: "cascade", }).notNull(), type: reactionType('type').notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("update_at").defaultNow().notNull(), }, t => [ primaryKey({ name: "video_reactions_p_key", columns: [t.userId, t.videoId], }) ])

复制代码

export const videoReactionsRelations = relations(videoReactions, ({ one }) => ({ users: one(users, { fields: [videoReactions.userId], references: [users.id], }), videos: one(videos, { fields: [videoReactions.videoId], references: [videos.id], }), }))

复制代码

export const videoReactionsInsertSchema = createInsertSchema(videoReactions) export const videoReactionsSelectSchema = createSelectSchema(videoReactions) export const videoReactionsUpdateSchema = createUpdateSchema(videoReactions)

在获取视频详细信息的接口中添加视频reactions相关的内容
  • 视频reactions相关的内容包含:当前视频的like数量、当前视频的dislike数量
  • .innerJoin(users, eq(videos.userId, users.id))
    • 在这个接口中,我们不仅要返回视频信息,还要返回视频发布者的用户信息,比如用户头像等,因此需要关联users表
    • innreJoin() SQL中的连接操作,只保留两张表都能匹配上的数据
复制代码

// src/modules/videos/server/procedure.ts export const videosRouter = createTRPCRouter({ getOne: baseProcedure .input(z.object({ videoId: z.uuid() })) .query(async ({ input, ctx }) => { const [video] = await db .select({ ... // 计算关联的videoReactions中type为'like'的数量 videoLikesCount: db.$count(videoReactions, and( eq(videoReactions.videoId, videos.id), eq(videoReactions.type, 'like') )), // 计算关联的videoReactions中type为'dislike'的数量 videoDislikesCount: db.$count(videoReactions, and( eq(videoReactions.videoId, videos.id), eq(videoReactions.type, 'dislike') )), }) .from(videos) .innerJoin(users, eq(videos.userId, users.id)) // 关联用户表,获取视频发布者的信息 .where(eq(videos.id, input.videoId)) }) })
*

在获取视频详细信息的接口中添加视频浏览者reaction相关的内容
  1. 判断浏览者是否登录: getOne这个接口我们使用的是baseProcedure,是不要求登录的;但如果要在页面中显示当前浏览者是否对视频有reaction的操作,只有登录状态下才能获取到

    复制代码

    const { clerkUserId } = ctx // 可选的,未登录时为undefined

  2. 在users表中找到当前浏览者viewer 这一步需要注意,可能存在用户未登录的情况,为了避免SQL报错,我们使用inArray()查询

    • inArray(column, array):相当于SQL的IN运算符,在我们这里的意思是选出users.clerkId等于clerkUserId的用户,如果clerkUserIdundefined则不匹配任何用户
    复制代码

    let viewerId; // 从users表中查找clerkUserId对应的用户,也就是当前浏览视频的用户 // 浏览者可能未登录,未登录状态下clerkUserId为undefined // inArray() 方法生成SQL的IN子句,确保即使clerkUserId为undefined时不会报错,只是匹配不到用户 const [viewer] = await db .select() .from(users) .where(inArray(users.clerkId, clerkUserId ? [clerkUserId] : [])) if (viewer) viewerId = viewer.id

  3. 获取当前浏览者对所有视频的reactions的数据并作为一张临时表

    • db.$with() Drizzle ORM里用来构建CTE(Common Table Expression,公用表表达式)的方法,我们这里CTE的名字是viewer_reactions,这个CTE只在这次查询中有效,不会真的建表,但后续的主查询中我们又可以像用表一样使用它
    复制代码

    // 创建一个子查询,获取当前浏览者对每个视频的reaction作为一个临时表 // 后续在主查询中通过leftJoin()获取浏览者对当前视频的reaction const viewerReactions = db.$with('viewer_reactions').as( db .select({ videoId: videoReactions.videoId, type: videoReactions.type }) .from(videoReactions) .where(inArray(videoReactions.userId, viewerId ? [viewerId] : [])) )

  4. 使用数据库查询获取数据并返回

    • leftJoin() SQL中的连接操作,关联左边表(我们这里是videos表),保留videos表的所有行,根据eq()进行条件判断,如果viewerReactions表中有匹配上的就带上这部分数据,没匹配上的则用null填充
    • groupBy() 按条件进行数据分堆,我们这里的意思是"每个视频-视频作者-当前浏览者对该视频的reaction"
    复制代码

    const [video] = await db .with(viewerReactions) // 使用上面定义的子查询 .select({ ... viewerReaction: viewerReactions.type // 当前浏览视频的用户对该视频的reaction类型 }) .from(videos) .innerJoin(users, eq(videos.userId, users.id)) // 关联用户表,获取视频发布者的信息 .leftJoin(viewerReactions, eq(videos.id, viewerReactions.videoId)) // 关联当前浏览视频的用户对视频的reactions .where(eq(videos.id, input.videoId)) .groupBy( videos.id, users.id, viewerReactions.type )

VideoReactionsRouter
  • 以like操作为例,基本逻辑是先检查用户对当前视频是否有过like操作,如果有则删除,如果没有like操作,则创建一条新的reaction,但需要注意的是用户虽然没有like操作,不代表没有dislike操作
  • onConflictDoUpdate({target: [...], set: {...}}) insert时避免唯一键冲突,根据target判断是否发生冲突,如果有冲突则更新set中要求更新的字段
复制代码

// src/modules/video-reactions/server/procedure.ts export const VideoReactionsRouter = createTRPCRouter({ like: protectedProcedure .input(z.object({ videoId: z.uuid() })) .mutation(async ({ ctx, input }) => { const { videoId } = input const { id: userId } = ctx.user // 检查用户是否已经对该视频有like reaction const [existingReaction] = await db .select() .from(videoReactions) .where(and( eq(videoReactions.userId, userId), eq(videoReactions.videoId, videoId), eq(videoReactions.type, 'like') )) // 如果已经有like reaction,则删除它(即取消点赞) if (existingReaction) { const [deletedReaction] = await db .delete(videoReactions) .where(and( eq(videoReactions.userId, userId), eq(videoReactions.videoId, videoId), eq(videoReactions.type, 'like') )) .returning() return deletedReaction } // 根据 userId + videoId 检查冲突,用户对该视频虽然没有like reaction,但可能有dislike reaction // 如果存在dislike reaction,则更新为like,否则插入新的like reaction const [createdReaction] = await db .insert(videoReactions) .values({ userId, videoId, type: 'like' }) .onConflictDoUpdate({ target: [videoReactions.userId, videoReactions.videoId], // 检查冲突的字段 set: { type: 'like' } // 如果有冲突需要更新的字段 }) .returning() return createdReaction }), dislike: protectedProcedure... })

相关推荐
wangruofeng1 小时前
Playwright 深度调研:为什么它成了浏览器自动化的新底座
前端·测试
小短腿的代码世界1 小时前
从.qrc到rcc编译器:Qt资源系统的隐秘运作机制与大型项目性能突围
开发语言·qt
2401_833269301 小时前
Java网络编程入门
java·开发语言
青瓦梦滋2 小时前
C++的IO流与STL的空间配置器
开发语言·c++
五月君_2 小时前
Bun v1.3.14 发布,Rust 版即将进 Claude Code 内测,下一版可能就告别 Zig
开发语言·后端·rust
李白的天不白3 小时前
SSR服务端渲染
前端
鱼很腾apoc3 小时前
【学习篇】第20期 超详解 C++ 多态:从语法规则到底层原理
java·c语言·开发语言·c++·学习·算法·青少年编程
XinZong3 小时前
OpenClaw 实现「龙虾」vs 龙虾 vs 用户 ws对话实现方案 + 实际落地项目
javascript
卷帘依旧4 小时前
WebSocket 比 SSE 复杂在哪里
javascript
不吃土豆的马铃薯4 小时前
4.SGI STL 二级空间配置器 allocate 与_S_refill 源码解析
c语言·开发语言·c++·dreamweaver·内存池