Tube - Video views

7 阅读2分钟
  • Video Description组件
    • 在这个组件中,我们展示视频的以下信息:浏览量、创建时间、description、按钮shou less/more
    • 整个组件我们设定了2种状态,收起 compact、展开 expand,以上的信息展示会分别适配2种状态
  • Schema
    • 明确视频、用户、浏览量之间的逻辑关系,用户和视频都可以对应多个浏览量,但一个浏览量只能由一个用户产生且只作用于一个视频
// video views
export const videoViews = pgTable("video_views", {
  userId: uuid("user_id").references(() => users.id, {
    onDelete: "cascade",
  }).notNull(),
  videoId: uuid("video_id").references(() => videos.id, {
    onDelete: "cascade",
  }).notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("update_at").defaultNow().notNull(),
}, t => [  primaryKey({    name: "video_views_pkey",    columns: [t.userId, t.videoId], // 联合主键,确保每个用户对每个视频只能有一个观看记录
  })
])

export const videoViewsRelations = relations(videoViews, ({ one }) => ({
  users: one(users, {
    fields: [videoViews.userId],
    references: [users.id],
  }),
  videos: one(videos, {
    fields: [videoViews.videoId],
    references: [videos.id],
  }),
}))
  • videoviews create
export const VideoViewsRouter = createTRPCRouter({
  create: protectedProcedure
    .input(z.object({ videoId: z.uuid() }))
    .mutation(async ({ ctx, input }) => {
      const { id: userId } = ctx.user
      
      const [existingVideoView] = await db
        .select()
        .from(videoViews)
        .where(and(
          eq(videoViews.userId, userId),
          eq(videoViews.videoId, input.videoId)
        ))

      if (existingVideoView) return

      const [createdVideoView] = await db
        .insert(videoViews)
        .values({
          userId,
          videoId: input.videoId
        })
        .returning()

      return createdVideoView
    })
})
  • db.$count()
    • Drizzle ORM的一个聚合函数,db.$count(table, condition),它会返回一个数字,表示满足condition的行数
    • 获取视频详细信息时返回视频浏览量,我们这里生成浏览量的逻辑是用户进入视频详情页点击视频播放时产生一次浏览
// src/modules/videos/server/procedure.ts

getOne: baseProcedure
.input(z.object({ videoId: z.uuid() })) 
.query(async ({ input }) => {
  const [video] = await db
    .select({  // 返回一个嵌套对象
      ...getTableColumns(videos),
      user: {...getTableColumns(users) },
      videoViews: db.$count(videoViews, eq(videoViews.videoId, videos.id)), // 计算关联的videoViews数量
    })
    .from(videos)
    .innerJoin(users, eq(videos.userId, users.id)) // 关联用户表,获取用户信息
    .where(eq(videos.id, input.videoId)) 
  if (!video) throw new TRPCError({ code: 'NOT_FOUND' })

  return video
}),
  • Hook useMemo()
    • 一个性能优化 Hook,在依赖不变的情况下,缓存函数返回值,避免不必要的重复计算
    • 当我们在子组件中拿到浏览量的数字时,会创建2种状态下的不同展示形式,也就是前面说的 compact、expand
interface VideoBannerProps {
  video: VideoGetOneOutput;
}

// views
const compactViews = useMemo(() => {
return Intl.NumberFormat('en', {
  notation: 'compact', // 紧凑型,如 1K, 1M
}).format(video.videoViews)
}, [video.videoViews])

const expandedViews = useMemo(() => {
return Intl.NumberFormat('en', {
  notation: 'standard', // 标准型,如 1,000, 1,000,000
}).format(video.videoViews)
}, [video.videoViews])