项目面.md

项目面

[toc]


乐享视频

点击使用慧言AI进行模拟面试


一、背景和参与人

此项目为在校兼职项目,帮助学长学姐完成毕设,成交价格2300;

共3人参与项目,前端vue,后端java,一人协助上线,我主要负责后端框架搭建,数据库设计,思路整理。

二、项目介绍

说到项目,其实是一个视频学习网站,整体设计参考B站;分为管理端和用户端,分免费和会员两类视频;基本实现视频管理/观看。会员充值/购买,用户评论/管理,笔记记录,点赞收藏等功能;整合第三方短信验证码服务,整合minio文件存储服务;最终实现线上部署及域名访问。

三、主要技术栈

  • spring boot
  • mysql数据库
  • mybatis plus持久层框架
  • java过滤器 + redis 权限认证
  • 阿里短信服务 + redis 实现验证码注册登录
  • lombok简化实体
  • swagger实时更新api文档
  • slf4j+logback日志打印

四、详解数据库设计

  1. 所有表均记录id、创建时间、更新时间
  2. user用户表:除了账号密码等常用字段外,因为涉及管理员/用户端区分,添加类型字段;因为涉及会员/非会员之分,添加是否会员 字段;因为需要模拟用户虚拟钱包支付,添加余额字段
  3. video视频表:主要字段有:主图路径(text)、视频路径(text)、类型;其次标注课程是否会员,记录点击次数;记录上下架状态
  4. reply回复表:暂为一级评论:主要标记视频id;标记回复人id;回复内容
  5. 其他较为简单的表:banner轮播图表,blog笔记表,collect收藏表等,主要均为标记视频与业务间关系

五、详解技术实现

java过滤器 + redis 权限认证:redis常用场景之一,此处键值设计为{token : userInfo},过期时间2小时;

  • 首先在用户登录时:
    1. 若可以通过账号密码查询到用户信息,也即登录成功;
    2. 生成唯一uuid作为token,当前用户信息转为json字符串;分别作为键值存入redis;
    3. 将生成的uuid存放如response的header中返回前端
  • 前端请求接口时:
    1. request的header携带登录获取的token,请求后端接口
  • 用户过滤器:
    1. 新建类实现spring的HandlerInterceptor接口
    2. 重写preHandle预处理方法,获取前端携带的token去redis查找用户信息
    3. 用户信息为空,说明用户退出或者token过期,限制访问;用户信息存在,放行接口,继续业务操作;
  • 注册/配置过滤器
    1. 新建配置类并使用@Configuration标注为配置类
    2. 新建WebMvcConfigurer方法,并使用@Bean注解,使其替换自带的mvc配置
    3. 方法中配置所有接口后,放行部分公共路径,如:登录,注册接口

阿里短信服务 + redis 实现验证码注册登录:第二个redis常用场景,此处键值设计为{tel : code},若有多个业务可标注接口名称如:{login_tel:code};有效时间5分钟

  • 用户点击获取验证码:
    1. 首先在后台生成随机六位数字
    2. 通过阿里短信sdk请求其接口,若返回结果ok,继续后续操作;否则抛出异常
    3. 将用户手机号、生成的随机六位数字分别作为键值存入redis
  • 用户登录/注册:
    1. 若用户使用验证码登录,需校验验证码code正确性
    2. 通过用户手机号从redis中获取redis值:redisCode
    3. 将用户输入的code与redisCode对比,若相同,登录/注册成功,否则失败

六、你在项目中遇到的难点,如何解决

  • 首先是如何鉴权:见 五、java过滤器 + redis 权限认证

  • 其次是多级评论设计:解决方式为:

    1. 在评论表中新增父级评论idp_id默认值0
    2. 一级列表展示:只需要查询所有p_id=0的评论列表
    3. 用户点击展开某个评论:只需要通过父级id查询子级评论即可
    4. 树形评论列表:只需要对评论列表进行递归查询子级,递归结束条件为其子级为null
    5. 参考代码:https://blog.csdn.net/tomisa/article/details/107933980
  • 类似的还有权限列表递归

    • 数据库设计
      image-20220628110059492
    • 返回实体结构
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      @Data
      public class PermissionTreeVO {
      /** 主键 */
      private Long id;
      /** 权限名 */
      private String perName;
      /** 权限路径 */
      private String perUrl;
      /** 权限类型 例如 0 菜单 1按钮 */
      private Integer perType;
      /** 父级权限 默认 0 */
      private Long parentId;
      /** 图标 */
      private String icon;
      /** 状态 0 禁用 1 启用 */
      private Boolean status;
      /** 描述 */
      private String remark;
      /** 子权限 */
      private List<PermissionTreeVO> childs;
      }
    • 具体实现
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      //controller
      /**
      * 通过parentId递归查询权限树(查询全部传0)
      */
      @ApiOperation(value = "通过parentId递归查询权限树(查询全部传0)")
      @PostMapping("/umsPermission/selectPermissionTreeByParentId/{parentId}")
      public Result selectPermissionTreeByParentId(@PathVariable Long parentId) {
      List<PermissionTreeVO> permissionTreeVOS = umsPermissionService.selectPermissionTreeByParentId(parentId);
      return Result.succeed(permissionTreeVOS, "查询成功");
      }

      // serviceImpl
      /**
      * @Description: 以输入的父级id寻找下级目录,只要下级目录不为空,就继续向下探寻,然后封装至上级的childs字段中
      * @param parentId
      * @return
      */
      @Override
      public List<PermissionTreeVO> selectPermissionTreeByParentId(Long parentId) {
      List<PermissionTreeVO> permissionList = umsPermissionMapper.selectPermissionTreeByParentId(parentId);
      if(permissionList!=null){
      for (PermissionTreeVO permission : permissionList) {
      permission.setChilds(selectPermissionTreeByParentId(permission.getId()));
      }
      }
      return permissionList;
      }

      // mapper.xml
      <select id="selectPermissionTreeByParentId" resultType="com.yk.i_wms.vo.PermissionTreeVO" parameterType="long">
      SELECT DISTINCT
      t.id id,
      t.per_name perName,
      t.per_url perUrl,
      t.per_type perType,
      t.icon icon,
      t.status status,
      t.remark remark,
      t.parent_id parentId
      FROM
      ums_permission t
      WHERE
      t.parent_id = #{parentId}
      order by t.id
      </select>

七、主要接口

  1. 注册:选择管理员或用户注册,可使用短信验证码注册,如此将生成默认密码;

  2. 登录:自动确定其为管理员还是用户;分短信验证码登录和密码登录,存储过程见 五、java过滤器 + redis 权限认证

  3. 退出:删除2中redis的token即可

  4. 发送验证码:见 五、阿里短信服务

  5. 文件上传:使用minio存储,项目引入minio-spring boot.jar;使用其sdk进行文件上传;并返回公网链接

  6. 会员充值:用户表:扣减余额并修改会员状态

  7. 评论:记录视频id,当前用户id,上级评论id

  8. 评论查询:见六、多级评论设计

  9. 收藏/收藏数:记录视频id,当前用户id;查询视频列表时count收藏数量

  10. 某用户是否收藏某视频的判断:用户点击视频,获取视频id和用户id,进行收藏表查询,若存在记录且收藏状态为已收藏;前端点亮收藏按钮,否则置灰收藏按钮

八、扩展

  1. MessageUtil/MsgController:短信服务工具类/接口
  2. MinioUtil/FileController:文件工具类/接口
  3. RedisUtils:redis 常用工具类
  4. LogAspect:日志切面,使用spring的aop思想,注解:@Aspect 对接口进行入参、出参、请求方式和路径的记录
  5. GlobalExceptionHandler:全局统一异常处理:使用注解@ControllerAdvice标注类,使用@ExceptionHandler标注方法进行异常捕捉
  6. SwaggerConfig:swagger配置
  7. Result:统一结果返回,使用静态方法简化结果返回

项目面.md
http://example.com/10802.html
作者
John Doe
发布于
2022年9月8日
许可协议