GQ Video

你们的服务是怎么做日志收集的

  • 我当前项目的的做法很简单:利用Logback,既打控制台,也打本地文件。每个服务把运行日志按固定格式写到本机的 logs 目录,按“日期+大小”自动切分,保留一段时间。出问题就直接看这些日志,定位很快。
  • 需要集中查看时,也不用改代码,只要在服务器上加个日志收集器Logstash、,把这些文件统一送到一个搜索平台(比如 ES),再用一个网页(比如 Kibana)来搜、画图、做告警就行。(也就是ELK)
  • 一句话流程:服务写文件 → 收集器把文件送到搜索库 → 网页上一搜就能看到所有服务的日志。

这样做的好处是:实现简单、出问题好排查;要升级成“像 ELK 那样的集中日志系统”也很容易,基本是平台侧加收集与展示,不用动业务代码。

项目中你是否使用过哪些 Java 并发工具类?

面试口述版(30-60 秒)

  • 我在 GQ Video 里主要用的是线程池配合队列做并发处理。比如我们用 ExecutorService 跑后台任务:一类线程消费 Redis 队列的“播放上报”和“转码任务”,把高峰请求削到后台,避免接口阻塞;还有删除视频这种重操作,我放到线程池里异步清理 ES、对象存储和本地文件,显著降低前端等待时间。
  • 传统的 JUC 工具像 ConcurrentHashMap、AtomicInteger、Semaphore、CountDownLatch、CyclicBarrier、BlockingQueue 在当前代码里没有直接大规模使用,但我熟悉它们的场景,业务上也有现成可落地的点。

追问时可展开(逐条举例)

  • 如果要引入 JUC,我会这样用:
    • ConcurrentHashMap:做热门视频的本地缓存与并发加载合并,避免同一时间多线程打 DB。
    • AtomicInteger/LongAdder:本地热点计数(在线人数、播放数滑窗),定时批量回写 Redis/DB。
    • Semaphore:限制“同时转码/同时上传/外部接口调用”并发度,保护 CPU/IO 和下游。
    • CountDownLatch:首页聚合多个区块数据并行拉取,主线程等待全部完成统一返回。
    • CyclicBarrier:多清晰度转码全部完成后再一次性发布“可播放”状态。
    • BlockingQueue:作为 Redis 或 MQ 不可用时的本地降级队列,保证生产-消费不断流。
  • 选择原则:共享可变状态优先用 ConcurrentHashMap/Atomic*;资源并发控制用 Semaphore;一次性并行聚合用 CountDownLatch/CompletableFuture;阶段同步用 CyclicBarrier;生产-消费优先消息中间件,退而求其次用 BlockingQueue+线程池。

一句话总结:现在我用“线程池+队列”把关键链路做了异步化和削峰,真要上更细粒度并发控制,我能把 JUC 工具按场景补进去,既能保证吞吐,也能控制稳定性。

当前项目哪里用到了AOP

在 xxx 这个项目里,我确实把 AOP 用在了两类典型的横切场景。

  • 第一类是“登录校验”的全局拦截。我做了一个自定义注解 @GlobalInterceptor,控制器方法只要打上这个注解并把 checkLogin=true,切面 GlobalOperationAspect 就会在方法执行前先跑一遍登录校验:从请求头里拿 token,到 Redis 里校验用户信息,不通过就直接抛业务异常。这样业务方法完全不用重复写登录判断,后期如果登录策略调整,只改切面一处就能全局生效。
  • 第二类是“把用户行为自动转成站内消息”。我定义了 @RecordUserMessage 注解,像收藏、评论、审核这类会触发通知的行为,只要在方法上打注解,UserMessageOperationAspect 会用 @Around 在方法成功返回后读取方法入参(比如 videoIdactionTypereplyCommentIdcontent),归一化成对应的 MessageType,然后调用消息服务落一条站内消息。失败或抛异常的情况不会记消息,保证幂等和一致性。管理端那边也有同名切面,做的事是一致的,只是调用方不同(通过 InteractClient 触发消息)。

这两处 AOP 的好处非常直观:

  • 一是把“登录校验”“行为转消息”这种跟业务强相关、但又应该统一收口的共性逻辑,从每个接口里抽出来集中管理,业务代码更干净;
  • 二是灵活性高,新增/调整规则都不需要改散落在各处的控制器;
  • 三是天然适合做“只在成功时记录”的事情,用 @Around 能拿到返回值与异常,更好地保证边界与一致性。

为什么用 AOP 而不是过滤器/拦截器:

  • 我会强调登录这类是“业务前置规则”,需要在方法级拿到注解、参数等上下文,AOP 在方法颗粒度更合适;
  • 网关 Filter/Servlet Filter/HandlerInterceptor 更偏协议层或路由层,做限流、跨域、鉴权粗粒度拦截很好,但像“成功后落一条站内消息”这种强业务语义,用切面在目标方法周围处理最自然。

最后,我也预留了扩展位:以后要做“操作审计”“性能埋点”“权限细粒度校验”,直接再加注解和切面就行,跟现有两套模式完全一致。

我看到你的GQ Video简历上提到了RocketMQ,你来讲讲你在这个项目上为什么用上了RocketMQ?相比没用RocketMQ有什么提升?你为什么考虑的是RocketMQ这个消息队列而不是RabbitMQ或者Kafka

下面这版适合面试口述,覆盖“为什么上 MQ、带来什么提升、为什么选 RocketMQ”。

  • 我们最初用的是“线程池 + Redis 列表”做异步,能用但有几个痛点:服务间耦合重、突发流量容易把线程和 Redis 顶满;失败重试、死信、消息追踪都要自己造轮子;一旦进程挂了,队列里的任务容易丢,排障也不直观。
  • 所以我把“播放事件统计、转码任务”这两条链路迁到了 RocketMQ。好处很直接:
    • 彻底解耦:发布方只管发,消费方独立扩缩容;
    • 抗峰值更稳:堆积可观测,消费端可以水平扩展;发布端响应时间明显变短;
    • 可靠性提升:自动重试、死信队列、顺序/延时/事务消息这些能力都是现成的,任务丢失概率大幅下降;
    • 可观测:Topic 堆积、耗时、轨迹一目了然,问题定位快。
  • 之所以选 RocketMQ,而不是 RabbitMQ 或 Kafka,我主要考虑了这个项目的特点:
    • 对 RabbitMQ:它路由模型很灵活,但吞吐相对没那么高;延时消息要装插件,顺序保证容易受限于单队列瓶颈。我们的“播放事件”和“转码发布”更偏大吞吐+延时/顺序/重试,RocketMQ 原生支持更合适。
    • 对 Kafka:吞吐极强,做日志/流式计算非常好。但业务型消息常用的“单条精确重试、死信、延时、事务消息”需要额外组件或自研,运维门槛也更高。我们更看重业务可靠投递和易用性,所以优先 RocketMQ。
  • 落地时我做了一个开关 mq.enabled:开启走 MQ,关闭就回退到原来的“线程池 + Redis 队列”,可以灰度切换、出问题一键回滚。幂等用数据库唯一键或 Redis 做了防重。

一句话总结:上 RocketMQ 后,我们把“重活、峰值、可靠性、可观测性”这些问题一次性解决了;对这个以业务消息为主的场景,RocketMQ 比 RabbitMQ 更省插件、比 Kafka 更贴合业务消息的开箱能力。

你具体讲讲播放事件统计、转码任务这两个任务,没上MQ你是怎么实现的,用了MQ你又是怎么实现的?

面试口语版回答(MQ 改造播放统计 & 转码任务)


一、没上 MQ 之前

那时候我们还没引入消息队列,用的是「线程池 + Redis 列表」来异步处理任务。

  • 播放事件
    前端上报播放,我这边直接把事件丢进 Redis 列表(LPUSH),后台有个线程池 while(true) 从列表里取(RPOP),
    做三件事:视频播放数 +1、写播放历史、同步 ES 计数。

    但问题挺多的:高峰期线程容易打满、Redis 队列堆积难监控,失败重试、死信都得自己写。进程挂了也不好排查。

  • 转码任务
    用户投稿上传完视频,我也是把转码任务信息塞进 Redis 列表,然后线程池轮询去拿,用 FFmpeg 转码、切片、传对象存储、最后更新数据库状态。
    同样的问题:并发不好控、延时触发不方便、重试补偿全靠自己维护。


二、上 MQ(RocketMQ)之后

后来我们用 RocketMQ 做异步解耦,效果挺明显。

我设计了两个 Topic:

  • video-play 专门处理播放事件
  • video-transcode 处理转码任务

然后写了一个通用发布器(Publisher),再配两个 Consumer。
另外加了个 mq.enabled 开关,可以灰度切换或快速回退到 Redis 方案。

  • 播放事件这块
    发布端接到上报后,直接把事件对象发到 MQ,不再阻塞主线程。
    消费端收到后,还是那三件事:数据库 +1、播放历史 upsert、ES 同步计数。
    可靠性方面 RocketMQ 自带重试、死信、堆积可查,我还做了幂等处理(比如用 userId+videoId+fileIndex 做唯一键)。
  • 转码任务这块
    用户提交稿件或合并分片后,我发一条转码消息到 video-transcode
    消费端收到后调转码组件,用 FFmpeg 转码、上传 MinIO、更新状态。
    并发我用 Semaphore 控制;异常重试、超出次数进死信队列,支持延时重试退避。

三、上 MQ 之后的变化

  • 性能上:主流程只负责发消息,响应速度更快。
  • 稳定性:堆积看得见,消费可水平扩,不容易被线程卡死。
  • 可靠性:MQ 自带重试、死信、轨迹,可观测性很好。
  • 扩展性:以后要加统计维度或新转码逻辑,只需多加个消费者,不改主流程。

一句话总结

以前用“线程池 + Redis 列表”能跑,但维护成本高、峰值容易崩;
上了 RocketMQ 后,我改成“发布订阅”模型,用它的重试、死信、延时和监控能力,把可靠性和扩展性都拉满,还能随时灰度回退,整体更稳更可控。


你的项目是怎么用ES实现高亮的?

  • 我们的高亮是用 ES 自带的 Highlighter 做的,场景是“视频搜索”。索引里主要搜两个字段:标题 videoName 和标签 tags
  • 查询的时候我在 DSL 里同时做两件事:一是 multi_match 搜这两个字段,二是加上 highlight 配置,指定高亮字段是 videoName,并设置前后缀标签,比如 <span class='highlight'></span>
  • ES 返回结果后,我从 hit.getHighlightFields() 里把高亮片段取出来,如果这个命中有高亮,就用它覆盖原来的标题,这样前端直接展示就是带 <span class='highlight'>关键词</span> 的字符串;前端只要给 .highlight 一个样式(比如黄色背景)就行。
  • 没有高亮的情况我就用原文回退,保证结果不空;排序这边除了相关性,还会叠加播放量等权重做二次排序。
  • 一句话总结:查询时让 ES 标注命中的关键词,返回时把高亮片段替换掉原字段,前端简单加个 CSS,用户就能一眼看到关键词出现的位置。

我看到你的项目技术栈有Elasticsearch ,你说说你的Elasticsearch 做了哪些事,你为什么选择了Elasticsearch这个搜索引擎?

口语化答案

  • 在 GQ Video 里,Elasticsearch主要干三件事:

    1. 做“视频搜索”的索引库,存标题videoName、标签tags、封面、作者等元数据;
    2. 支持关键词高亮、相关性排序和多维排序(在相关性基础上叠加播放量/收藏量等热度字段);
    3. 实时更新计数类字段(播放/弹幕/收藏),用脚本自增并带 upsert,保证字段不存在也不报错。
  • 搜索怎么用的:

    • 查询走 multi_match 命中标题+标签,打开 highlighter,让命中的词用<span class='highlight'>包起来,前端直接渲染即可;
    • 排序先按相关性 _score,再按业务字段(比如播放量)做二次排序;
    • 分页、统计这些都在 ES 里完成,接口只负责把 videoId 批量回表补全必要信息。
  • 和 MySQL 的关系:

    • MySQL是主库,ES是“搜索副本”,我们走“最终一致”:新增/修改/删除异步同步到 ES;计数类通过消息/队列异步自增,失败有重试和兜底重刷。
  • 为什么选 Elasticsearch:

    • 全文检索体验好:倒排索引+相关性评分,模糊匹配、同义词、分词(IK)都很成熟;
    • 性能与扩展性强:大数据量下的查询/分页稳定,分片副本易于横向扩展;
    • 生态完善:高亮、聚合、脚本更新、排序、统计都开箱即用,开发成本低、可维护性好。

一句话:ES在项目里就是“高性能的搜索引擎 + 实时热度排序器”,把复杂的检索、高亮、排序交给它处理,MySQL只专注事务数据,二者异步同步,既快又稳。

你项目中的注册/登录验证码是怎么实现的?你是怎么解决“当其他用户同时发起请求验证码时,上一个用户的验证码就会被覆盖掉”这个问题的?

  • 我这边的验证码是“算术验证码”。用户点开注册/登录时,后端生成一张图片(ArithmeticCaptcha,100×42),把图片转成 base64 给前端展示。
  • 存储不放 Session,而是放 Redis。每次生成都会先造一个唯一的 checkCodeKey(UUID),再用键名 easylive:checkCode:{checkCodeKey} 保存真实验证码,设置 10 分钟过期。
  • 前端拿到两个字段:checkCode(base64 图片)和 checkCodeKey。提交表单时把用户输入的验证码和 checkCodeKey 一起传回来。
  • 校验时,后端用 checkCodeKey 从 Redis 取出真实验证码,忽略大小写比对;无论成功失败,都在 finally 里删除这个键,防止重复使用(一次一用)。
  • “多人同时请求会互相覆盖”的问题,就是以前大家都往同一个 Redis Key(比如 checkCode)里写,后来改成“每次生成一个独立的 checkCodeKey”,天然隔离不同用户/不同请求,就不会互相顶掉了。
  • 细节与安全:
    • 过期自动失效(TTL);
    • 建议配合限流(同 IP/账号单位时间内次数限制);
    • 也可以把 checkCodeKey 和会话/设备指纹做一次绑定,防止被拿去复用;
    • 校验后立刻清空,防重放。

简单讲讲这个项目的注册功能?

  • 前端流程:用户填邮箱、昵称、密码,先过图片验证码;注册时会带上验证码的 key 和用户输入值提交。
  • 后端校验:
    • checkCodeKey 去 Redis 取真实验证码,比对后立刻删除(一次一用)。
    • 查库判断邮箱、昵称是否已存在,存在就抛业务异常。
  • 创建用户:
    • 生成 10 位 userId,密码用 MD5 加密,设置默认状态/性别/主题与加入时间;
    • 初始化硬币(当前/累计各 10)。
  • 落库与返回:调用 userInfoMapper.insert(...) 入库,成功后给前端“注册成功”的提示,前端切换到登录页(不自动登录)。
  • 风险与保证:
    • 并发重复注册:代码层面先查重,线上建议配合邮箱/昵称唯一索引兜底;
    • 验证码隔离:每次生成独立 checkCodeKey 存 Redis,互不覆盖,且设置过期与一次性校验;
    • 安全:限制同 IP/邮箱的注册频率,密码存库仅存加密后值。

简单讲讲这个项目的登录以及自动登录、退出你是怎么实现的?

  • 登录怎么做

    • 前端把邮箱、密码、图片验证码一起提交(带上 checkCodeKey)。
    • 后端先用 checkCodeKey 去 Redis 取真实验证码,比完立刻删掉(一次一用)。
    • 校验用户:查邮箱、比对 MD5 密码、校验账号状态;记录最后登录时间和 IP。
    • 生成登录态:用 UUID 生成一个 token,把用户信息+过期时间(7 天)塞进 Redis(key=token:{token})。
    • 下发给前端:把 token 写到 Cookie(有效期 7 天、path=/)。如果之前已有 Cookie 的旧 token,会顺手清掉 Redis 里的旧 token(保证单端生效)。
  • 自动登录怎么做

    • 前端带着 Cookie 里的 token 调用“自动登录”。
    • 后端用 token 去 Redis 拿到用户信息;如果快过期(<1 天),顺延过期时间并回写 Redis,同时重新写 Cookie(续期)。
    • 返回用户信息即可,前端无感知。
  • 退出怎么做

    • 读取 Cookie 里的 token,删除 Redis 中对应登录态;
    • 同时把 Cookie 置 0 过期,浏览器端清掉。
  • 小结

    • 核心就是“Redis 存 token + Cookie 持有 token”,登录写入、自动登录续期、退出清理;验证码用 Redis + 独立 key 防覆盖,保证一次一用。

简单讲讲这个项目的分类功能是怎么实现的,一级分类和二级分类是如何实现的?

  • 数据结构

    • 一张表 category_info 管分类:category_idp_category_idcategory_code(唯一)category_nameiconbackgroundsort
    • 约定:p_category_id=0 是一级分类;p_category_id=某一级ID 的就是它的二级分类。
  • 新增/修改

    • 保存前先按 category_code 做唯一校验,防止重复。
    • 新增时同父级下取最大 sort,当前分类设为 max+1;修改时按主键更新即可。
  • 删除

    • 支持“级联删除”:传入一个 categoryId,同时把该分类以及它的直接子分类一并删掉(SQL 用 category_id = ? OR p_category_id = ?)。
  • 排序

    • 提供一个批量排序接口,前端把拖拽后的 ID 顺序传上来(如 "3,5,4"),后端按顺序重写同父级下的 sort 值。
  • 查询与树形

    • 查询时按 sort asc 取出线性列表;如果入参标记 convert2Tree=true,后端把线性数据按 p_category_id 组装成树(给每个节点挂 children),前端直接渲染成“一级-二级”的层级结构。
  • 缓存

    • 分类变更后会刷新 Redis 缓存,把“排好序的树形列表”整体存到 category:list:;客户端读取时优先走缓存,未命中再回源并回填缓存。

一句话:用 p_category_id 建父子关系(0 表示一级),唯一校验+排序号维护新增/修改,删除支持带子类,查询可直接返回树形并缓存到 Redis,前端据此渲染一级/二级分类。

简单讲讲这个项目的文件上传功能是怎么实现的

  • 预上传建档

    • 前端先调“预上传”,把文件名和分片数发过来。
    • 服务端生成一个 uploadId,顺带在磁盘建临时目录,在 Redis 里记录会话状态(分片总数、当前进度、临时路径、TTL),返回 uploadId。
  • 分片上传

    • 前端按顺序带着 uploadId + chunkIndex + 分片文件逐片上传。
    • 后端校验:会话是否存在、是否跳片、大小是否超限;然后把分片落到“临时目录/chunkIndex”这个文件里。
    • 每传一片就把进度与累计字节数写回 Redis,并续期,支持断点续传与重传当前片(覆盖写)。
  • 合并与后续处理

    • 所有分片到齐后(前端或服务端触发“合并”),把临时分片顺序拼成完整视频文件。
    • 触发后续流程:转码/切片/上传对象存储(或本地),回写数据库状态与文件路径;这一步我们支持走 MQ(RocketMQ)异步处理,峰值更稳,也可开关回退到线程池+Redis 队列。
  • 安全与体验

    • 上传限额、单文件大小、分片并发数都可配置;路径做了合法性校验。
    • 返回的是相对路径,前端用统一的“读文件”接口访问;封面图可选生成 200px 缩略图加速展示。

一句话:用“预上传建档(Redis 会话)+ 顺序分片上传(断点续传)+ 合并 + 异步转码/入库”的流程,既稳又能抗大文件与高并发。

面试官问:简单讲讲这个项目的审核功能是怎么实现的

口语化答案(精简)

  • 审核入口

    • 管理端调一个接口 /auditVideo,带3个参数:videoIdstatus(通过/驳回)、reason(可空)。
  • 关键校验

    • 我先做枚举校验,只允许把“待审核”改为“通过”或“驳回”。
    • 用“乐观锁”防止多人同时操作:更新时带条件 where video_id=? and status=待审核(2),不是待审核就更新不了,相当于只让第一个点的人成功。
  • 通过后的动作

    • 把“投稿表”的最新数据拷贝到“线上表”(insertOrUpdate)。
    • 文件清单也同步:先删线上旧清单,再把投稿文件清单整体复制过去。
    • 清理这次投稿产生的“待删除文件队列”,避免遗留临时/旧文件。
    • 后续:写入 ES、刷新分类缓存、发站内消息等。
  • 驳回的动作

    • 只更新投稿状态和理由,不落线上表。
  • 事务与幂等

    • 整个审核方法开事务,任一步失败会回滚;
    • 乐观锁保证“只审核一次”,天然幂等。

一句话:审核就是“带乐观锁的状态流转”。通过就把投稿数据/文件清单拷到线上并清理残留;驳回只改状态与理由。用事务+乐观锁,既防并发踩踏,也保证数据一致性。

简单讲讲这个项目的弹幕功能是怎么实现的

  • 存储设计:一张 video_danmu 表记录弹幕(videoId/fileId/userId/text/mode/color/time/postTime),配合 video_info 的计数字段做统计。
  • 发送流程:前端带 videoId/fileId/text/mode/color/time/postDanmu;后端从登录态取 userId,做长度校验(≤200),判断视频存在且未关闭弹幕,入库后把 video_info 的弹幕数 +1
  • 展示流程:播放器按分P的 fileId/loadDanmu,后端按 danmu_id asc 查询该分P的弹幕列表返回;若视频“关闭弹幕”标识开启则直接返回空列表。
  • 开关与计数:视频的 interaction 字段控制“关闭弹幕”;计数通过 updateCountInfo(videoId, 'danmu_count', 1) 累加(同时记录播放等统计)。
  • 要点:登录校验走全局拦截;每条弹幕带时间戳 time,前端按播放进度渲染;字段简单(文本/颜色/模式)便于前端直接绘制。

你是怎么保证弹幕在指定时间出现的

  • 存储层:每条弹幕都有一个 time 字段(单位秒,表示在第几秒出现),按同一分P的 fileId 管理,接口返回时按 time/ID 升序。
  • 播放层:用 Artplayer 的弹幕插件,初始化喂数据为 [{ time, text, color, mode }]。插件以 video.currentTime 为唯一时间源,内部用 rAF/时间窗口比对(≈±0.2s)到点即渲染;倍速跟随 playbackRate,暂停/恢复会暂停/继续调度。
  • 跳转与换 P:监听 seeked 事件清屏并用二分/指针把读游标跳到 currentTime 附近,继续按序推送;切换 fileId 重新拉该分P的弹幕。
  • 误差控制:后端返回秒级;前端在发送时用 Math.floor(player.currentTime) 写入,展示用小窗口匹配避免网络/解码抖动;弱网时预取下一段(例如 10–20s)并本地缓存,确保到点能显示。
  • 边界与一致性:只以播放器时间轴为准,不依赖服务器时间;同一视频的“关闭弹幕”由后端标志直接返回空列表,前端不再调度。

简单讲讲这个项目的点赞收藏投币功能是怎么实现的

  • 数据模型与幂等

    • 用一张 user_action 表记录用户对视频的行为(点赞/收藏/投币等),有“视频ID+评论ID+行为类型+用户ID”的唯一索引,天然防重复。
    • 视频侧在 video_info 里维护计数字段(like_count、collect_count、coin_count 等)。
  • 接口与流程

    • 前端调 /userAction/doAction,传 videoId、actionType、actionCount(投币用),后端从登录态取 userId 组装 UserAction
    • 点赞/收藏是“开关型”:
      • 如果这条行为已存在→删除记录,视频计数 -1;
      • 如果不存在→插入记录,视频计数 +1。
    • 投币是“累加型”:
      • 禁止给自己视频投币;同一视频不可重复占用同一笔(查到已投则报错或按规则限制);
      • 扣用户当前硬币,给 UP 增加硬币;写入一条投币行为;视频投币数 +N。
    • 全过程包事务,任何一步失败整体回滚。
  • 展示与消息

    • 视频详情接口会把“当前用户对该视频的已发生行为列表”一起返回,前端据此把点赞/收藏按钮置蓝等。
    • 方法上加了 @RecordUserMessage,通过 AOP 在业务成功后自动落一条站内消息(如被点赞/被收藏提醒),业务代码无需重复编写通知逻辑。
  • 风控与细节

    • 账号状态/参数合法性校验;actionCount 限制;数据库侧 coin 增减用“余额不得小于 0”的 where 条件兜底;
    • 计数字段统一走 updateCountInfo(videoId, field, change),避免分散更新导致不一致。

一句话:点赞/收藏用“存在删、无则增”的开关模式;投币用“扣我加你+行为落表”的累加模式;表唯一索引保证幂等,视频计数统一维护,AOP 自动发站内消息,整体在一个事务里保证一致性。

简单讲讲这个项目的评论功能是怎么实现的?父级子级评论怎么实现的?

  • 存储设计
    • 一张 video_comment 表,核心字段:comment_idp_comment_id(0=父评/主评,>0=子评归属的父评ID)、video_iduser_idreply_user_idcontentlike_counthate_counttop_typepost_time
  • 发评论
    • 接口 /postComment,前端传 videoId、content,可带 replyCommentId
    • 后端取登录用户,校验视频存在且未关闭评论。
    • 若是回复:查被回复的那条评论;顶层就 p_comment_id=被回复ID,回复子评则沿用其顶层 p_comment_id,并写 reply_user_id
    • 入库;若是主评(p_comment_id=0)则给视频 comment_count +1
  • 查评论(含父子结构)
    • 接口 /loadComment,默认只查主评(p_comment_id=0),分页返回,排序支持“点赞数优先/时间”。
    • 开启 loadChildren=true 时,Mapper 用 <collection> 一次把每条主评的 children(子评列表)查出来并挂载,前端拿到的就是树形结构。
    • 首屏附加置顶:第一页把 top_type=1 的评论置顶到最前。
  • 点赞/踩
    • 用户对同一条评论“点赞/踩”互斥;再次点击同一动作为取消;计数字段增减一起维护。
  • 删除与置顶权限
    • 删除:评论作者本人或视频 UP 可删;删主评会级联删其子评,并把视频 comment_count -1
    • 置顶/取消置顶:仅视频 UP 可操作;置顶前会先清空本视频其它置顶,保证唯一。

一句话:用 p_comment_id 建父子关系(0=主评、>0=子评),发评论按是否回复决定 p_comment_idreply_user_id,查评论时主评分页+一次性挂载 children 返回树形;再配合点赞/踩、删除、置顶等周边能力。

置顶评论怎么实现的?

  • 我用一个字段做标记:video_comment.top_type,0=未置顶,1=置顶。
  • 权限:只有视频的 UP 主能置顶/取消置顶(服务里先根据 commentId 找到 videoId,再校验 videoInfo.userId == 当前用户)。
  • 接口与实现:
    • 置顶 /topComment:先调用“取消置顶”,把该视频下所有 top_type=1 批量改成 0(确保“只有一条置顶”);再把目标评论 top_type 更新为 1。整个过程放在事务里,避免并发下出现两条置顶。
    • 取消置顶 /cancelTopComment:根据 commentId 找到所属视频,把该视频下 top_type=1 的评论批量置为 0。
  • 展示:加载评论时,如果是第一页,会单独查一遍 top_type=1 的置顶评论(可带子评),把它去重后插到列表最前面返回给前端。

在线人数怎么实现的?

  • 我用“心跳+过期监听”的思路做的,核心都在 Redis。
  • 客户端每隔几秒上报一次心跳:调用后端 /video/reportVideoPlayOnline?fileId&deviceId
  • 服务器做两件事:
    • 首次心跳:给这个用户写一个在线标记 key,比如 video:play:online:user:{fileId}:{deviceId},TTL≈8s;同时把在线总数 key video:play:online:count:{fileId} 自增1(并给它TTL≈10s)。返回当前在线数。
    • 非首次心跳:只给这两个 key 续期,不再自增,避免一个人被计多次。
  • 下线/断网的扣减:
    • 开启 Redis key 过期事件(Ex),监听到“用户在线标记”过期时,根据 key 解析出 fileId,对 video:play:online:count:{fileId} 做自减1。
  • 这样就实现了“首上+1、持续续期、不重复计数、心跳断了自动-1”,进程宕机也能靠过期回收,计数基本准。

24小时热门显示是怎么实现的?

  • 我这块没做复杂的离线计算,走的是“在线统计 + SQL 过滤排序”。
  • 每次播放都会把 video_info.play_count +1,同时更新 last_play_time=now()(在 updateCountInfo 里做的)。
  • 接口 /video/loadHotVideoList 查询最近 24 小时:SQL 条件 last_play_time >= now() - interval 24 hour,再按 play_count desc 排序,分页返回,并联表补上作者头像/昵称做展示。
  • 这样能实时反映最近一天的热度;如果要换窗口(如 48 小时/7 天)只改查询参数即可。配合 last_play_timeplay_count 索引,查询很快。

你的项目中哪里用到了AOP?

在 GQ Video 这个项目里,我确实把 AOP 用在了两类典型的横切场景。

  • 第一类是“登录校验”的全局拦截。我做了一个自定义注解 @GlobalInterceptor,控制器方法只要打上这个注解并把 checkLogin=true,切面 GlobalOperationAspect 就会在方法执行前先跑一遍登录校验:从请求头里拿 token,到 Redis 里校验用户信息,不通过就直接抛业务异常。这样业务方法完全不用重复写登录判断,后期如果登录策略调整,只改切面一处就能全局生效。

  • 第二类是“把用户行为自动转成站内消息”。我定义了 @RecordUserMessage 注解,像收藏、评论、审核这类会触发通知的行为,只要在方法上打注解,UserMessageOperationAspect 会用 @Around 在方法成功返回后读取方法入参(比如 videoIdactionTypereplyCommentIdcontent),归一化成对应的 MessageType,然后调用消息服务落一条站内消息。失败或抛异常的情况不会记消息,保证幂等和一致性。管理端那边也有同名切面,做的事是一致的,只是调用方不同(通过 InteractClient 触发消息)。

这两处 AOP 的好处非常直观:

  • 一是把“登录校验”“行为转消息”这种跟业务强相关、但又应该统一收口的共性逻辑,从每个接口里抽出来集中管理,业务代码更干净;
  • 二是灵活性高,新增/调整规则都不需要改散落在各处的控制器;
  • 三是天然适合做“只在成功时记录”的事情,用 @Around 能拿到返回值与异常,更好地保证边界与一致性。

为什么用 AOP 而不是过滤器/拦截器:

  • 我会强调登录这类是“业务前置规则”,需要在方法级拿到注解、参数等上下文,AOP 在方法颗粒度更合适;
  • 网关 Filter/Servlet Filter/HandlerInterceptor 更偏协议层或路由层,做限流、跨域、鉴权粗粒度拦截很好,但像“成功后落一条站内消息”这种强业务语义,用切面在目标方法周围处理最自然。

最后,我也预留了扩展位:以后要做“操作审计”“性能埋点”“权限细粒度校验”,直接再加注解和切面就行,跟现有两套模式完全一致。