文章目录
- 前言
- 回顾
- 完成任务
- 1. 管理端登录
- 登录校验
- 2. 分类管理
- 2.1 分类列表
- 2.2 保存分类
- 2.3 删除分类
- 2.4 改变排序
- 2.5 刷新缓存
- 3. 文件上传
- 3.1 上传图片
- 3.2 获取图片资源

前言
本项目非原创,我只是个小小白,跟随 b 站脚步,找到老罗的这个项目,视频来源于:
高仿B站(单服务版) springboot项目实战 easylive
本人不分享项目源码,支持项目付费!!!
回顾
昨天完成了用户端的登录注册功能。成功登入界面后,界面上应该会出现视频的分类,而视频的分类是由管理员来处理的,所以要先进行管理端的相关操作。
完成任务
1. 管理端登录
controller 层:
管理端登录其实与客户端相差不大,都需要将 token 存入 Redis,将 token 存入 Cookie,成功登录后也同样需要删除 Redis 中的验证码(管理端获取验证码操作与客户端一样),删除 Redis 中的 token。
注意:管理端和客户端不能使用相同的 token 字段,要区分开。
登录校验
要进入管理后台,进行一系列操作的前提都是必须先登录。如果没有登录,就直接访问管理端后台的地址,这是不允许的。所以,需要使用拦截器。
– 客户端的登录校验的差异性比较大,直接访问页面可能不需要登录,点赞、评论这些才需要登录,所以,到时候用 AOP 来实现。
设计一个配置类来配置 Spring MVC 的相关行为:
实现了 WebMvcConfigurer 接口,该接口提供了多种用于配置Spring MVC的回调方法。
appIntercepter 实例为拦截器,拦截所有路径下的请求。
AppIntercepter 类,用于拦截请求并执行预处理逻辑:
- HandlerInterceptor 接口:是Spring MVC提供用于处理请求拦截的。
- if (!(handler instanceof HandlerMethod)) 条件为真的话,为什么返回 true ?——> 检查handler是否为HandlerMethod的实例,如果不是,允许请求继续处理。意味着请求不是针对Controller方法的,可能是静态资源请求
- 在前面的配置类中,我们要求的拦截的是所有路径,但实际上,一开始的获取验证码和登录都不应该拦截。所以,对于 URI 中包含 “/account” 的请求,要允许继续处理。
- 针对文件的请求,可能需要从 Cookie 中获取 token,因为文件请求可能不带 head 头,无法通过 getHeader 获取 token。
- 如果 Redis 根据 token 获取不到用户信息,说明 token 失效(登录超时)。
当页面关闭后,再次打开管理端登录页面,要保证需要进行重新登录,而不是还能继续访问关闭之前的页面。对 token 保存到 Cookie 中时,设置过期时间为 -1:
为什么设置为 -1 ? 因为这样设置,token 的过期时间会表示为 “会话”。也就是说只有在当前会话中才能继续访问,换一个会话(页面),token 就会失效:
2. 分类管理
2.1 分类列表
controller 层:
根据排序号 sort 升序排序,并需要将查询到的扁平化的分类数据转为树的形式。因为分类由一级分类和二级分类。
service 层:
通过递归调用 convertLine2Tree() 方法,逐层构建每个分类的子树,更好地表示层级关系。
例如,原本查询的数据应该为:
[{"categoryId": 1, "pCategoryId": 0, "name": "一级分类A", "children": null},{"categoryId": 2, "pCategoryId": 1, "name": "二级分类A-1", "children": null},{"categoryId": 3, "pCategoryId": 1, "name": "二级分类A-2", "children": null},
]
转为树形结构:
[{"categoryId": 1,"pCategoryId": 0,"name": "一级分类A","children": [{"categoryId": 2,"pCategoryId": 1,"name": "二级分类A-1","children": []},{"categoryId": 3,"pCategoryId": 1,"name": "二级分类A-2","children": []}]}
]
2.2 保存分类
controller 层:
service 层:
根据分类编号查询数据库,分析分类编号已存在的情况(分类编号必须是唯一的),并抛出异常。
分类 ID 如果不存在,查询表中最大的排序号,将这个最大的排序号+1后作为该分类的排序号;如果分类 ID 已经存在,说明是进行分类的修改。
2.3 删除分类
controller 层:直接调用 service 层实现
service 层:
删除的时候,因为分类是有两级的,如果一级分类下的 A 删除,那么 A 下的二级分类 B、C … 也应该删除。
所以,某分类的父级分类的 ID 是 categoryId 的话,也需要进行对其进行删除。
2.4 改变排序
controller 层:
service 层:
获取的分类 ID 参数 categoryIds 是 String 类型,为每个对象设置一个排序号,调用 mapper 中的方法批量更新每个分类对应的排序。
2.5 刷新缓存
在每次完成对数据的修改,无论是新增分类、删除分类、还是改变排序,最后都会进行 save2Redis() 的操作,这个操作是用来刷新缓存。将最新的数据同步到 Redis 缓存中。
3. 文件上传
3.1 上传图片
在保存分类的时候,有两个可选项是上传图标和背景图:
这里就涉及文件上传。
controller 层,上传图片:
区分月份来保存文件,将要上传的文件通过 transferTo() 方法保存到新的文件路径下。
根据传递的参数,判断是否要生成缩略图,如果为 true,就要通过 ffmpegUtils 工具类的 createImageThumbnail() 方法生成缩略图:
(前提是,电脑上必须下载配置 FFmpeg。配置成功在电脑中 cmd,输入 “ffmpeg -version” ,应该会出现如下样式:
生成缩略图的方法中还需要 ProcessUtils 工具类来执行 executeCommand 方法,安全地在不同操作系统上执行外部命令(特别是 FFmpeg)。这里我直接粘贴这个类,便于以后使用:
java">public class ProcessUtils {private static final Logger logger = LoggerFactory.getLogger(ProcessUtils.class);private static final String osName = System.getProperty("os.name").toLowerCase();public static String executeCommand(String cmd, Boolean showLog) throws BusinessException {if (StringTools.isEmpty(cmd)) {return null;}Runtime runtime = Runtime.getRuntime();Process process = null;try {//判断操作系统if (osName.contains("win")) {process = Runtime.getRuntime().exec(cmd);} else {process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});}// 执行ffmpeg指令// 取出输出流和错误流的信息// 注意:必须要取出ffmpeg在执行命令过程中产生的输出信息,如果不取的话当输出流信息填满jvm存储输出留信息的缓冲区时,线程就回阻塞住PrintStream errorStream = new PrintStream(process.getErrorStream());PrintStream inputStream = new PrintStream(process.getInputStream());errorStream.start();inputStream.start();// 等待ffmpeg命令执行完process.waitFor();// 获取执行结果字符串String result = errorStream.stringBuffer.append(inputStream.stringBuffer + "\n").toString();// 输出执行的命令信息if (showLog) {logger.info("执行命令{}结果{}", cmd, result);}return result;} catch (Exception e) {logger.error("执行命令失败cmd{}失败:{} ", cmd, e.getMessage());throw new BusinessException("视频转换失败");} finally {if (null != process) {ProcessKiller ffmpegKiller = new ProcessKiller(process);runtime.addShutdownHook(ffmpegKiller);}}}/*** 在程序退出前结束已有的FFmpeg进程*/private static class ProcessKiller extends Thread {private Process process;public ProcessKiller(Process process) {this.process = process;}@Overridepublic void run() {this.process.destroy();}}/*** 用于取出ffmpeg线程执行过程中产生的各种输出和错误流的信息*/static class PrintStream extends Thread {InputStream inputStream = null;BufferedReader bufferedReader = null;StringBuffer stringBuffer = new StringBuffer();public PrintStream(InputStream inputStream) {this.inputStream = inputStream;}@Overridepublic void run() {try {if (null == inputStream) {return;}bufferedReader = new BufferedReader(new InputStreamReader(inputStream));String line = null;while ((line = bufferedReader.readLine()) != null) {stringBuffer.append(line);}} catch (Exception e) {logger.error("读取输入流出错了!错误信息:" + e.getMessage());} finally {try {if (null != bufferedReader) {bufferedReader.close();}if (null != inputStream) {inputStream.close();}} catch (IOException e) {logger.error("调用PrintStream读取输出流后,关闭流时出错!");}}}}
}
3.2 获取图片资源
- 先判断 sourceName 是否是一个有效的路径,通过 pathIsOk() :这里主要是为了阻止访问比当前路径层级更高的目录。
- 设置正确的响应内容类型和缓存控制头,这里的缓存时间设置为 30 天,意味着客户端可以缓存该资源30天,不需要每次都向服务器请求。
- 调用 readFile 方法,根据 sourceName 找到对应的文件,并将其内容读取出来,然后通过 HttpServletResponse 发送给客户端。
·这个读取文件的操作过程,我也直接粘贴,便于以后使用。(主要就是通过文件输入流和文件输出流)java">protected void readFile(HttpServletResponse response, String filePath) {File file = new File(appConfig.getProjectFolder() + Constants.FILE_FOLDER + filePath);if (!file.exists()) {return;}try (OutputStream out = response.getOutputStream(); FileInputStream in = new FileInputStream(file)) {byte[] byteData = new byte[1024];int len = 0;while ((len = in.read(byteData)) != -1) {out.write(byteData, 0, len);}out.flush();} catch (Exception e) {log.error("读取文件异常", e);}}