基于 SpringBoot 的 OnlineMusicPlayer 项目
- 2022 年 8 月 11 日 江西
本文字数:38050 字
阅读完需:约 125 分钟
@TOC
项目演示
项目部署链接地址:
项目演示:
创建项目
因为我们的online-music-player项目是基于SpringBoot框架开发的,所以我们需要创建一个SpringBoot项目!
选择SpringBoot版本并初步导入依赖!
数据库的设计
根据我们的演示我们可以得知我们需要创建onlinemusic数据库,其下有 3 张结果!
创建
onlinemusic数据库创建
user用户表创建
music音乐列表创建
lovemusic收藏音乐列表onlinemusic数据库下的 3 张表创建成功后!
3 张表结构如下!
整个db.sql文件
-- 创建onlinemusic数据库 drop database if exists onlinemusic; create database if not exists onlinemusic character set utf8;-- 使用onlinemusicuse onlinemusic;-- 创建user表drop table if exists user;create table user ( id int primary key auto_increment, -- 设置自增主键 id username varchar(20) not null, -- 用户名不能为空! password varchar(255) not null -- 这里密码不为空,长度255留有足够长度加密操作);-- 创建music表drop table if exists music;create table music( id int primary key auto_increment, title varchar(50) not null, -- 歌曲名称 singer varchar(30) not null, -- 歌手 time varchar(13) not null, -- 添加时间 url varchar(1000) not null, -- 歌曲路径 user_id int(11) not null);-- 创建lovemusic表drop table if exists lovemusic;create table lovemusic( id int primary key auto_increment, user_id int(11) not null, -- 用户id music_id int(11) not null -- 音乐id);
配置数据库和 xml
打开Application.properties文件进行配置
#配置数据库spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&serverTimezone=UTCspring.datasource.username=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.jdbc.Driver#配置xmlmybatis.mapper-locations=classpath:mybatis/**Mapper.xml#配置springboot上传文件的大小,默认每个文件的配置最大为15Mb,单次请求的文件的总数不能大于100Mbspring.servlet.multipart.max-file-size = 15MBspring.servlet.multipart.max-request-size=100MB# 配置springboot日志调试模式是否开启debug=true# 设置打印日志的级别,及打印sql语句#日志级别:trace,debug,info,warn,error#基本日志logging.level.root=INFOlogging.level.com.example.onlinemusic.mapper=debug#扫描的包:druid.sql.Statement类和frank包logging.level.druid.sql.Statement=DEBUGlogging.level.com.example=DEBUG
登入注册模块设计
创建 User 类
我们先创建一个model包用来保存实体类
在其下创建User类!
package com.example.onlinemusic.model;import lombok.Data;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 13:37 */@Data //Data注解生成了setter/getter/tostring方法public class User { private int id; private String username; private String password;}
创建对应的 Mapper 和 Controller
创建 UserMapper 接口
创建Mapper包保存 Mapper 接口!
//UserMapperpackage com.example.onlinemusic.mapper;
import com.example.onlinemusic.model.User;import org.apache.ibatis.annotations.Mapper;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 13:38 */@Mapper //实现xml映射,无需通过其他的mapper映射文件!public interface UserMapper { //登入功能! User login(User loginUser);}
创建 UserMapper.xml
在resource包下创建mybatis包用于保存 mapper.xml 文件,再创建UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.onlinemusic.mapper.UserMapper"> <select id="login" resultType="com.example.onlinemusic.model.User">select * from user where username=#{username} and password=#{password}</select></mapper>
实现登入
设置登入的请求和响应
请求
响应
响应:{ "status":0, //status 为0表示登入成功,为负数表示登入失败! "message":"登入成功", // 放回登入信息! "data":{ // 登入成功后获取到相应的用户信息! "id":xxxx, "username":xxxx, "password":xxxx }}
创建 UserController 类
创建一个controller包,在其包下创建一个 UserController 类
package com.example.onlinemusic.controller;
import com.example.onlinemusic.mapper.UserMapper;import com.example.onlinemusic.model.User;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 15:12 */@RestController // @ResponseBody + @Controller@RequestMapping("/user") //设置路由 用来映射请求!public class UserController { //将UserMapper注入! @Resource private UserMapper userMapper;
@RequestMapping("/login") public void login(@RequestParam String username,@RequestParam String password){ User userLogin = new User(); userLogin.setUsername(username); userLogin.setPassword(password);
User user = userMapper.login(userLogin); //先初步测试一下,后面再完善 if(user!=null){//登入成功! System.out.println("登入成功!"); }else{ System.out.println("登入失败!"); } }}
这里只是粗略的写一下登入逻辑,然后验证是否可行,我们再进行后续代码的完善!
pastman 验证登入功能
我们对照数据库中的user表中的数据!所以该登入请求成功!
封装响应
刚刚我们的登入逻辑是没有了问题,但是我们的服务器并没有给客户端返回响应,所以我们需要根据约定的响应,登入请求后分装并返回!
创建一个tools包统一保存一些通用的代码,创建响应类ResponseBodyMessage这里响应信息我们可以通过泛型,就可以变成通用的响应!
package com.example.onlinemusic.tools;import lombok.Data;/** * Created with IntelliJ IDEA. * Description:统一(泛型)的响应体 * User: hold on * Date: 2022-07-26 * Time: 21:32 */@Datapublic class ResponseBodyMessage <T>{ private int status;//状态码 0 表示成功,-1表示失败! private String message;//响应信息描述 private T data; //返回的数据,这里采用泛型因为响应的数据的种类很多 public ResponseBodyMessage(int status, String message, T data) { this.status = status; this.message = message; this.data = data; }}
验证
Session 创建
我们再对刚刚的登入功能创建Session
我们通过HttpServlet下的 getSession 方法获取到 Session,然后再通过SetAttribute方法设置会话,保存在服务器中!
//优化后的UserController类!package com.example.onlinemusic.controller;
import com.example.onlinemusic.mapper.UserMapper;import com.example.onlinemusic.model.Contant;import com.example.onlinemusic.model.User;import com.example.onlinemusic.tools.ResponseBodyMessage;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 15:12 */@RestController // @ResponseBody + @Controller@RequestMapping("/user") //设置路由 用来映射请求!public class UserController { //将UserMapper注入! @Resource private UserMapper userMapper;
@RequestMapping("/login") //@RequestParam SpringMVC下的注解,表示该参数必须传入给服务器! //value = "前端参数名",required = true/false,defaultValue ="默认值" //这里required设置为ture表示该参数必传,默认为true,如果设置了defaultValue那默认required为false public ResponseBodyMessage<User> login(@RequestParam String username, @RequestParam String password, HttpServletRequest request){ User userLogin = new User(); userLogin.setUsername(username); userLogin.setPassword(password); //调用mapper下的 login查询user! User user = userMapper.login(userLogin); //返回响应 if(user!=null){//登入成功! //登入成功就在服务器保存该Session会话! //这里我们的key值可以通过常量值设置,避免后面出错! //request.getSession().setAttribute("USERINFO_SESSION_KEY",user); // request.getSession().setAttribute(Contant.USERINFO_SESSION_KEY,user); return new ResponseBodyMessage<User> (0,"登入成功",userLogin); }else{ return new ResponseBodyMessage<User> (-1,"登入失败",userLogin); } }}
我们通过 Fiddler 抓包获取响应,
设置了Session会话响应
未设置Session会话响应!
我们可以看到设置了的返回的响应头中有 Session 信息,否则没有!
Bcrypet 加密原理
我们知道如果我们登入时传输的密码通过明文传输的话,就不安全,会被其他人盗取,所以我们要对密码进行加密!
目前主流的加密方式有 2 种
MD5 加密
Bcrypet 加密
我们先对这两种加密方式进行了解,便于后续我们对登入功能进行加密操作!
MD5 加密
MD5 加密是一个安全的散列算法(哈希算法),就是通过对某一密码进行哈希操作,然后得到哈希后的加密字符串密码,一般这里加密后密码的长度比原来密码长度长,我们这里的加密操作是不可逆的!所以当我们对一个密码进行 MD5 加密后得到的字符串,我们无法通过逆操作解密,所以相对安全.
MD5 的不足之处,在于我们每次对同一个密码加密后得到的结果都是固定值,这就是使得,我们可以通过彩虹表(密码本,里面记录了密码加密算法后得到结果的映射关系),我们通过彩虹表查询进行暴力破解,就可以拿到密码!
我们也可以对 MD5 加密进行优化,就是对密码进行加盐操作,再进行 MD5 散列算法,这里的加盐指的是对密码添加一些单词,也就是字符,通过加盐操作,使得密码长度更长也就更安全,MD5 加密后得到的结果也就更难破解!
如果我们要使用 MD5 加密,我们要在项目中导入 MD5 依赖
<!-- md5 依赖 --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency>
模拟 MD5 加密操作:
package com.example.onlinemusic.tools;
import org.apache.commons.codec.digest.DigestUtils;
/** * Created with IntelliJ IDEA. * Description:对密码加盐后再md5 * User: hold on * Date: 2022-07-26 * Time: 23:28 */public class MD5Util { //定义一个固定的盐值 private static final String salt = "1b2i3t4e"; public static String md5(String src) { return DigestUtils.md5Hex(src); } /** * 第一次加密 :模拟前端自己加密,然后传到后端 * @param inputPass * @return */ public static String inputPassToFormPass(String inputPass) { String str = ""+salt.charAt(1)+salt.charAt(3) + inputPass +salt.charAt(5) + salt.charAt(6); return md5(str); } /** * 第2次MD5加密 * @param formPass 前端加密过的密码,传给后端进行第2次加密 * @param salt 用户数据库当中的盐值 * @return */ public static String formPassToDBPass(String formPass, String salt) { String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4); return md5(str); } /** * 上面两个函数合到一起进行调用 * @param * @param saltDB * @return */ public static String inputPassToDbPass(String inputPass, String saltDB) { String formPass = inputPassToFormPass(inputPass); String dbPass = formPassToDBPass(formPass, saltDB); return dbPass; } public static void main(String[] args) { System.out.println("对用户输入密码进行第1次加密:"+inputPassToFormPass("123456")); System.out.println("对用户输入密码进行第2次加密:"+formPassToDBPass(inputPassToFormPass("123456"), "1b2i3t4e")); System.out.println("对用户输入密码进行第2次加密:"+inputPassToDbPass("123456", "1b2i3t4e")); }}
虽然这里的加盐操作使得加密后的密码长度更长了,但是还是解决不了 md5 对一个密码加密得到的结果相同,除非我们这里采用随机盐!
BCrypet 加密
这里的BCrypet加密方式也是一种安全的不可逆的散列算法加密操作,BCrypet加密和MD5不同之处在于每次对同一个密码加密得到的结果都不相同,也就是在其内部实现了随机加盐处理.这就很好解决了MD5加密的缺点.所以BCrypet加密方式更加安全!并且BCrypet加密可以使加密得到的密文长度最大为 60 位,而 MD5 是 32 位,所以相对于 MD5 加密,BCrypet破解难度更大!
使用BCrypet加密:
引入依赖:
<!-- security依赖包 (加密)--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId></dependency>
//BCrypet加密使用演示package com.example.onlinemusic.tools;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 23:42 */public class BCrypetTest { public static void main(String[] args) { //模拟从前端获得的密码 String password = "123456"; BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String newPassword = bCryptPasswordEncoder.encode(password); System.out.println("加密的密码为: "+newPassword); //使用matches方法进行密码的校验 boolean same_password_result = bCryptPasswordEncoder.matches(password,newPassword); //返回true System.out.println("加密的密码和正确密码对比结果: "+same_password_result); boolean other_password_result = bCryptPasswordEncoder.matches("987654",newPassword); //返回false System.out.println("加密的密码和错误的密码对比结果: " + other_password_result); }}
这里
BCrypet加密工具主要通过BCrypetPassWordEncoder对象下的encode加密方法对密码进行加密和matches匹配算法通过密码和加密后的密文进行匹配!
可以看到这里每次加密的结果都不一样,但是都能和正确密码匹配成功!
MD5 和 BCrypet 的异同
MD5:一种不加盐的单向 hash,不可逆的加密算法,对同一个密码每次 hash 加密得到的 hash 值结果都是一样的!所以大多数情况下可以破解!
BCrypet:一种加盐的单向 Hash,不可逆的加密算法,每次对同一个密码进行加密的结果不同,破解难度更高!
这 2 个都是目前主流的加密算法,BCrypet 更加安全,但是效率低! BCrypet 的加盐操作是加入的随机盐,所以每次的加密结果都不一样!
指的注意的是并没有什么密码是绝对安全的,无论那种加密方式都可以被破解的,只是破解的成本和时间问题!如果你的数据并没有价值,那么破解你的密码就毫无意义,也就很安全!
加密登入实现
因为我们 matches 通过前端传输过来的密码对比数据库中密码即可判断密码是否正确!我们知道每次 encode 后的密码都不一样,所以我们不能通过查询数据库中 username+password 验证!我们先通过 username 查询到数据库中的密码,然后通过 mathes 匹配判断是否登入成功!
UserMapper 添加查询用户方法!
//加密后的登入方法!public ResponseBodyMessage<User> login(@RequestParam String username, @RequestParam String password, HttpServletRequest request){ User user = userMapper.selectUserByUserName(username); //返回响应 if(user!=null){//查询到username用户! BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); //匹配验证密码是否正确! boolean flg = bCryptPasswordEncoder.matches(password,user.getPassword()); if(flg){//登入成功! request.getSession().setAttribute(Contant.USERINFO_SESSION_KEY,user); return new ResponseBodyMessage<User> (0,"登入成功",user); }else{//密码错误! return new ResponseBodyMessage<User> (0,"用户名或密码错误",user); }
}else{//用户不存在! return new ResponseBodyMessage<User> (-1,"用户名或密码错误",user); } }
pastman 验证代码:
这里我们发现失败了,因为我们引入BCrypet加密依赖时,导入的security框架,我们只是用了其下的一个类用于加密,并没有用到该框架的功能!而导入security框架后,该项目中的接口都需要身份验证和授权!所以这个我们就登入失败了!
我们在启动类上加上一行注解即可解决该问题!
@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
创建 config 类,添加 AppConfig 类
刚刚BCrypetPasswordEncoder对象创建使用方式并不符合SpringIoC思想,所以我们通过Bean注解先将该对象注册到Spring中,然后通过Spring获取对象!
@Configurationpublic class AppConfig { //将BCrypetPasswordEncoder对象交给spring管理! @Bean public BCryptPasswordEncoder getBCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); }}
这样我们的登入功能就完善好了!
实现注册功能
约定注册请求和响应
请求:{ post url:user/register data:{username,password}}
响应:{ status:"状态码" message:"响应信息" data:{username,password}}
UserMapper 类中添加一个注册接口!
//注册功能! int register(String username,String password);
Mapper.xml 下实现该接口
<!-- 注册添加用户--> <insert id="register"> insert into user (username,password) values(#{username},#{password}); </insert>
Controller 的 User 类下实现注册功能
//注册功能 @RequestMapping("/register") public ResponseBodyMessage<User>register(@RequestParam String username,@RequestParam String password,HttpServletRequest request){ //1.首先查询该用户是否存在! User user = userMapper.selectUserByUserName(username); if(user!=null){//查询到该用户,说明用户存在! return new ResponseBodyMessage<User>(-1,"该用户已注册,请修改用户名重新注册",null); } //2.用户不存在,就注册该用户! //对密码进行加密后保存在数据库中! password = appConfig.getBCryptPasswordEncoder().encode(password); //将用户注册到数据库中! userMapper.register(username,password); //将注册好的用户信息放回给客户端! user = userMapper.selectUserByUserName(username); request.getSession().setAttribute(Contant.USERINFO_SESSION_KEY,user); return new ResponseBodyMessage<User>(0,"注册成功!",user); }
postman 验证
用户存在
用户不存在注册成功!
通过注册的用户验证一下登入功能
上传音乐模块
上传音乐模块设计
上传音乐请求和响应
请求:{ post, url:music/upload data:{singer,MultipartFile file} //上传音乐的歌手名和音乐文件}
响应:{ status:0,//0表示成功,-1失败! message:"响应信息", data:true //true表示成功 }
Music 类
package com.example.onlinemusic.model;
import lombok.Data;
/** * Created with IntelliJ IDEA. * Description:Music实体类 * User: hold on * Date: 2022-07-27 * Time: 15:37 */@Datapublic class Music { private int id; private String title; private String singer; private String time; private String url; private int user_id;}
MusicController 类
这里MusicController类中的上传方法,需要处理 2 部分内容
将音乐文件上传到服务器下
将上传的音乐信息上传到数据库中
上传到服务器
package com.example.onlinemusic.controller;
import com.example.onlinemusic.model.Contant;import com.example.onlinemusic.tools.ResponseBodyMessage;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.multipart.MultipartFile;import sun.util.logging.resources.logging;
import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpSession;import java.io.File;import java.io.IOException;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-27 * Time: 15:40 */@RestController@RequestMapping("/music")public class MusicController {
// //文件上传服务器后的路径地址!// public static final String SAVE_PATH = "D:/uploadmusicfile/";
//我们可以将该路径信息设置到配置文件中! @Value("${music.path.save}") private String SAVE_PATH; //这里的上传需要将音乐上传到服务器,还有就是需要将音乐信息上传到数据库! @RequestMapping("/upload") public ResponseBodyMessage<Boolean> UploadMusic(String singer, MultipartFile file, HttpServletRequest request){
//1.上传音乐前验证登入状态,如果未登入就不创建会话 //如果用户已登入,则允许上传音乐! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return new ResponseBodyMessage<>(-1,"未登录,请登录后上传!",false); } //2.登入状态,可以将歌曲上传到服务器!
//获取到音乐文件名 xxx.mp4 String musicname = file.getOriginalFilename(); System.out.println("musicfileAndtype:"+musicname); //上传文件在服务器下保存的路径! String path = SAVE_PATH + musicname; //创建文件对象 File desc = new File(path); //该文件目录在磁盘中不存在,就创建该目录 if(!desc.exists()){ desc.mkdir(); } //将音乐文件上传到该目录下! try { file.transferTo(desc); } catch (IOException e) { e.printStackTrace(); //上传失败 return new ResponseBodyMessage<>(-1,"上传失败",false); } //上传成功 return new ResponseBodyMessage<>(0,"上传成功",true); }}
postman 验证
未登录上传文件
登入后上传
可以看到我们设置的服务器目录下就含有了该上传的音乐文件!
如何保证上传的文件是音乐文件
可以通过后缀名.MP3嘛?
虽然这是一种最简单的解决方案,但是显然不可以的,如果有人将不是
.MP3文件改成后缀为.MP3的音乐文件,不过这种情况比较罕见!
那么我们如何检测用户上传的是音乐文件呢?
其实每一种文件都有自己特点的文件结构!
我们拿
.MP3文件举例:文件结构如下:
![]()
一个 MP3 文件结构分成 3 部分!
而确定是否是 MP3 文件可以通过
MPEG音频标签![]()
例如我们通过
ID3V1128 字节中的前 3 个字节中的标签标志包含了字符TAG就可以判断该文件是MP3文件了!
增加音乐文件校验功能
我们先创建一个文件校验的接口,测试一下:
//音乐文件验证 @RequestMapping("/ismp3") public ResponseBodyMessage<String> ismp3(String path){ String str = null; File file = null; byte [] fileByte = null; try { file= new File(SAVE_PATH+File.separator+path); //获取到这个文件的所有字节信息 fileByte = Files.readAllBytes(file.toPath()); } catch (IOException e) { e.printStackTrace(); } //将字节数组转成字符串 str = new String(fileByte); //获取到最后128个字节的字符串信息! str = str.substring(str.length()-128);
if(str.contains("TAG")){//最后128字节,含有音乐文件标志 return new ResponseBodyMessage<String>(0,"mp3文件",str); } //没有音乐文件标识 return new ResponseBodyMessage<String>(-1,"非mp3文件",str); }
验证:
音乐文件:
非音乐文件:
测试该方法无误,我们就将其封装到上传音乐的模块中!
//1.先验证是否为音乐文件! boolean flg = false; try { //获取到后128位含有标志字节的字符串 String str = new String(file.getBytes()); String flgTAG = str.substring(str.length()-128); if(flgTAG.contains("TAG")){ //含有标志位,为音乐文件! flg = true; } } catch (IOException e) { e.printStackTrace(); } if(!flg){//不是音乐文件 return new ResponseBodyMessage<>(-1,"文件有误,并非mp3音乐文件",false); }
验证:
音乐文件上传成功:
非音乐文件:
上传到数据库
我们上传音乐信息到数据库,就是向数据库中的music表中插入数据!
我们首先要明确我们需要到数据库那些信息!
我们看一下我们music表结构!
id:自增主键不需要上传!title:歌曲名我们可以通过文件名去掉
.MP3后缀获取! singer:歌手 我们请求信息中有!time:我们可以通过java中的SimpleDateFormat类获取到上传时间 url:音乐的url,因为我们上传的音乐就是用来后面播放的嘛,而我们数据的传输是通过http协议的,我们后面通过这个
url就可以找到该音乐的位置!//先用这样的方式保存url /music/get?title(歌曲名称)user_id:我们可以通过
session中获取上传用户id
MusicMapper
接口
package com.example.onlinemusic.mapper;
import org.apache.ibatis.annotations.Mapper;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-28 * Time: 23:27 */@Mapperpublic interface MusicMapper { //上传音乐
/** * * @param title 文件名去后缀得到音乐名 * @param singer * @param time 通过SimpleDateFormat类获取到上传时间! * @param url 便于后面播放! * @param user_id 通过session获取 * @return */ int upload(String title,String singer,String time,String url,String user_id);}
xml 实现
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.onlinemusic.mapper.MusicMapper"> <!--上传一条音乐信息到数据库--> <insert id="upload"> insert into music (title,singer,time,url,user_id) values(#{title},#{singer},#{url},#{user_id}) </insert></mapper>
SimpleDateFormat 类和 Date 类获取系统时间并格式化
package com.example.onlinemusic.tools;
import java.text.SimpleDateFormat;import java.util.Date;
/** * Created with IntelliJ IDEA. * Description:SimpleDateFormat格式化时间类学习! * User: hold on * Date: 2022-07-28 * Time: 23:43 */public class GetTimeTest { public static void main(String[] args) { //我们可以通过 java.utilev包下的Date类获取到当前系统时间! Date currentTime = new Date(); System.out.println(currentTime);
//获取时间格式化类 //年月日 y M d //时分秒 H m s //通过构造方法传入需要设置的时间格式 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //将当前时间设置成你需要的格式! String time = dateFormat.format(new Date()); System.out.println(time); }}
new Date() 获取到当前系统时间
SimpleDateFormat 类 对时间进行格式化处理
yyyy-MM-dd HH:mm:ss //年-月-日 时:分:秒
MusicController 完善数据库上传
package com.example.onlinemusic.controller;
import com.example.onlinemusic.mapper.MusicMapper;import com.example.onlinemusic.model.Contant;import com.example.onlinemusic.model.Music;import com.example.onlinemusic.model.User;import com.example.onlinemusic.tools.ResponseBodyMessage;import org.apache.ibatis.annotations.Mapper;import org.springframework.beans.factory.annotation.Value;import org.springframework.http.ResponseEntity;import org.springframework.jdbc.datasource.SimpleDriverDataSource;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.multipart.MultipartFile;import sun.util.logging.resources.logging;
import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpSession;import java.io.File;import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.text.SimpleDateFormat;import java.util.Date;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-27 * Time: 15:40 */@RestController@RequestMapping("/music")public class MusicController {
// //文件上传服务器后的路径地址!// private String SAVE_PATH = "D:/uploadmusicfile/";
//我们可以将该路径信息设置到配置文件中! @Value("${music.path.save}") private String SAVE_PATH;
//上传音乐到数据库的url前缀 @Value("${music.url}") private String URL_PRE;
@Resource //属性注入 private MusicMapper musicMapper;
//这里的上传需要将音乐上传到服务器,还有就是需要将音乐信息上传到数据库! @RequestMapping("/upload") //当我们没有传歌手信息时,就默认为未知歌手 public ResponseBodyMessage<Boolean> UploadMusic(@RequestParam(defaultValue = "未知歌手") String singer, @RequestParam(value = "filename") MultipartFile file, HttpServletRequest request){
//1.上传音乐前验证登入状态,如果未登入就不创建会话 //如果用户已登入,则允许上传音乐! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return new ResponseBodyMessage<>(-1,"未登录,请登录后上传!",false); } //2.登入状态,可以将歌曲上传到服务器!
//获取到音乐文件名 xxx.mp4 String musicname = file.getOriginalFilename(); System.out.println("musicfileAndtype:"+musicname);
//上传歌曲前验证歌曲是否已经存在! String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //获取到当前用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); Music music = musicMapper.getMusicByUidAndMusicInfo(title,singer,user_id); if(music!=null){ //说明该歌曲重复上传 System.out.println(title+"已存在"); return new ResponseBodyMessage<>(-1,"重复上传,歌曲已存在",false); } //歌曲未上传 //上传文件在服务器下保存的路径! String path = SAVE_PATH + musicname; //创建文件对象 File dest = new File(path); //该文件目录在磁盘中不存在,就创建该目录 if(!dest.exists()){ dest.mkdir(); System.out.println("mkdir"+dest); } //将音乐文件上传到该目录下! try { file.transferTo(dest); System.out.println(dest); } catch (IOException e) { e.printStackTrace(); //上传失败 return new ResponseBodyMessage<>(-1,"服务器上传失败",false); } //服务器上传成功,我们就需要对数据库进行上传信息!
//1.数据准备 //1).title 通过文件名截取到歌名,验证上传重复上传时已获取title //String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //2).singer 直接获取用户上传的 singer //3).time SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String time = simpleDateFormat.format(new Date()); //4).url 可以通过拼接! eg: music/get?path=隆里电丝 String url = URL_PRE+title; //5).user_id 通过当前session获取,验证重复上传时已获取// User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY);// int user_id = user.getId(); int ret = musicMapper.upload(title,singer,time,url,user_id); if(ret!=1){//上传失败! //我们数据库上传失败,那么就需要将服务器下的该音乐文件删除 System.out.println("服务器上传成功,数据库上传失败,删除服务器下文件"); dest.delete(); return new ResponseBodyMessage<>(-1,"数据库上传失败",false); } return new ResponseBodyMessage<>(0,"上传成功",true); } //实现支持多个文件上传就将MultipartFile改成数组即可! @RequestMapping("/uploads") public ResponseBodyMessage<Boolean> UploadMusics(@RequestParam(defaultValue = "未知歌手") String singer, @RequestParam(value = "filename") MultipartFile[] files, HttpServletRequest request){
//1.上传音乐前验证登入状态,如果未登入就不创建会话 //如果用户已登入,则允许上传音乐! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return new ResponseBodyMessage<>(-1,"未登录,请登录后上传!",false); } //2.登入状态,可以将歌曲上传到服务器!
//保存上传失败的歌曲信息 StringBuilder uploadfailinfo = new StringBuilder(); for (MultipartFile file:files) {
//获取到音乐文件名 xxx.mp4 String musicname = file.getOriginalFilename(); System.out.println("musicfileAndtype:"+musicname);
//上传歌曲前验证歌曲是否已经存在! String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //获取到当前用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); Music music = musicMapper.getMusicByUidAndMusicInfo(title,singer,user_id); if(music!=null){ //说明该歌曲重复上传 System.out.println(title+"已存在"); //保存歌曲信息,用于返回前端用户! uploadfailinfo.append(title+","); //进行下一首歌曲上传 continue; } //歌曲未上传 //上传文件在服务器下保存的路径! String path = SAVE_PATH + musicname; //创建文件对象 File dest = new File(path); //该文件目录在磁盘中不存在,就创建该目录 if(!dest.exists()){ dest.mkdir(); System.out.println("mkdir"+dest); } //将音乐文件上传到该目录下! try { file.transferTo(dest); System.out.println(dest); } catch (IOException e) { e.printStackTrace(); //上传失败 return new ResponseBodyMessage<>(-1,"服务器上传失败",false); } //服务器上传成功,我们就需要对数据库进行上传信息!
//1.数据准备 //1).title 通过文件名截取到歌名,验证上传重复上传时已获取title //String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //2).singer 直接获取用户上传的 singer //3).time SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String time = simpleDateFormat.format(new Date()); //4).url 可以通过拼接! eg: music/get?path=隆里电丝 String url = URL_PRE+title; //5).user_id 通过当前session获取,验证重复上传时已获取// User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY);// int user_id = user.getId(); int ret = musicMapper.upload(title,singer,time,url,user_id); if(ret!=1){//上传失败! //我们数据库上传失败,那么就需要将服务器下的该音乐文件删除 System.out.println("服务器上传成功,数据库上传失败,删除服务器下文件"); dest.delete(); return new ResponseBodyMessage<>(-1,"数据库上传失败",false); }
} if(uploadfailinfo.length()==0) { //说明全部歌曲上传成功! return new ResponseBodyMessage<>(0, "上传成功", true); } //部分歌曲上传失败 return new ResponseBodyMessage<>(0,"歌曲:"+uploadfailinfo+"已存在,上传失败,"+"其他歌曲上传成功",true);
}}
验证:
查看数据库music表
一个用户重复上传一首歌曲解决
显然我们的上传功能还有待优化,我们需要解决一个用户多次上传一首歌曲的行为!
在验证用户登入的行为后,再进行该用户上传的音乐文件是否已经上传过的验证
我们通过查询数据库信息,从而验证
MusicMapper 接口
/** * 通过用户id和音乐信息验证是否重复上传 * @param title * @param singer * @param user_id * @return */ Music getMusicByUidAndMusicInfo(String title, String singer, int user_id);}
xml 实现
<select id="getMusicByUidAndMusicInfo" resultType="com.example.onlinemusic.model.Music"> select * from music where title=#{title} and singer=#{singer} and user_id=#{user_id} </select>
Controller 重复上传验证
//上传歌曲前验证歌曲是否已经存在! String title = musicname.substring(musicname.lastIndexOf(".mp3")); //获取到当前用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); Music music = musicMapper.getMusicByUidAndMusicInfo(title,singer,user_id); if(music!=null){ //说明该歌曲重复上传 System.out.println("重复上传"); return new ResponseBodyMessage<>(-1,"上传失败,歌曲已存在",false); }
postman 验证
批量上传歌曲
我们可以将参数file改成MulitspartFile的数组,就可以一次性批量上传多首歌曲
但是我们这里的歌手信息就无法全部上传咯!
//实现支持多个文件上传就将MultipartFile改成数组即可! @RequestMapping("/uploads") public ResponseBodyMessage<Boolean> UploadMusics(@RequestParam(defaultValue = "未知歌手") String singer, @RequestParam(value = "filename") MultipartFile[] files, HttpServletRequest request){
//1.上传音乐前验证登入状态,如果未登入就不创建会话 //如果用户已登入,则允许上传音乐! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return new ResponseBodyMessage<>(-1,"未登录,请登录后上传!",false); } //2.登入状态,可以将歌曲上传到服务器!
//保存上传失败的歌曲信息 StringBuilder uploadfailinfo = new StringBuilder(); for (MultipartFile file:files) {
//获取到音乐文件名 xxx.mp4 String musicname = file.getOriginalFilename(); System.out.println("musicfileAndtype:"+musicname);
//上传歌曲前验证歌曲是否已经存在! String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //获取到当前用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); Music music = musicMapper.getMusicByUidAndMusicInfo(title,singer,user_id); if(music!=null){ //说明该歌曲重复上传 System.out.println(title+"已存在"); //保存歌曲信息,用于返回前端用户! uploadfailinfo.append(title+","); //进行下一首歌曲上传 continue; } //歌曲未上传 //上传文件在服务器下保存的路径! String path = SAVE_PATH + musicname; //创建文件对象 File dest = new File(path); //该文件目录在磁盘中不存在,就创建该目录 if(!dest.exists()){ dest.mkdir(); System.out.println("mkdir"+dest); } //将音乐文件上传到该目录下! try { file.transferTo(dest); System.out.println(dest); } catch (IOException e) { e.printStackTrace(); //上传失败 return new ResponseBodyMessage<>(-1,"服务器上传失败",false); } //服务器上传成功,我们就需要对数据库进行上传信息!
//1.数据准备 //1).title 通过文件名截取到歌名,验证上传重复上传时已获取title //String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //2).singer 直接获取用户上传的 singer //3).time SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String time = simpleDateFormat.format(new Date()); //4).url 可以通过拼接! eg: music/get?path=隆里电丝 String url = URL_PRE+title; //5).user_id 通过当前session获取,验证重复上传时已获取// User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY);// int user_id = user.getId(); int ret = musicMapper.upload(title,singer,time,url,user_id); if(ret!=1){//上传失败! //我们数据库上传失败,那么就需要将服务器下的该音乐文件删除 System.out.println("服务器上传成功,数据库上传失败,删除服务器下文件"); dest.delete(); return new ResponseBodyMessage<>(-1,"数据库上传失败",false); }
} if(uploadfailinfo.length()==0) { //说明全部歌曲上传成功! return new ResponseBodyMessage<>(0, "上传成功", true); } //部分歌曲上传失败 return new ResponseBodyMessage<>(0,"歌曲:"+uploadfailinfo+"已存在,上传失败,"+"其他歌曲上传成功",true); }
验证:
上传音乐模块总结
上传包括服务器和数据库上传
这里上传文件用到了
Spring框架中处理文件上传的主要类MulitspartFile类,这个类主要实现的是前端用表单的方式进行提交!
上传服务器时要验证音乐是否重复上传,这里通过查询数据库中的信息进行验证
在上传音乐时验证是否为
MP3文件,我们通过MP3文件结构中的最后 128 个字节下有一个TAG标签即可验证
播放音乐模块设计
请求响应设计
请求:{ get /music/get?path=xxx.mp3}
响应:{ data:音乐数据本身的字节信息}
可以看到我们这里请求的设计采用的是
get方法,通过/music/get?path=xxx.mp3获取到响应的音乐信息,这也就和我们之前设计的保存音乐的url匹配上了!而我们响应只要将对应的音乐字节信息返回给浏览器即可!
代码实现
//播放音乐 @RequestMapping("/get") public ResponseEntity<byte[]> get(String path){ //我们要先获取到该音乐保存在服务器下的路径信息! try { byte[] fileByte = null; //获取到文件对象 File file = new File(SAVE_PATH +File.separator+ path); //获取到该文件对象的路径信息 Path filepath = file.toPath(); //读取文件中的所有字节 fileByte = Files.readAllBytes(filepath); return ResponseEntity.ok(fileByte); }catch (IOException e){ e.printStackTrace(); } return ResponseEntity.badRequest().build(); }
方法讲解
Files.readAllBytes(Path path);读取文件中的所有字节,参数是
Path路径值!File.separator与系统相关的默认名称 - 分隔符字符,以方便的方式表示为字符串!
就是在
Windows系统下是\,而在Linux下是/ReponseEntity这是
Spring对请求响应的分装,继承了HttpEntity对象,包含Http响应码(HttpStatus),响应头(header),响应体(body)3 部分!
ResponseEntity类继承自HttpEntity类,被用于 Controller 层方法 !我们可以通过这个类下面提供的静态方法,封装一下响应返回给浏览器!
ok静态方法:我们代码里的
ResponseEntity.ok(fileByte);就是将ok状态和fileByte音乐文件信息以body的形式返回给前端!
验证结果
音乐文件有TAG标志

假如我们拿到的并不是mp3文件!
这里的隆里电丝.mp3我是通过png文件改成了这个,显然找不到这个音乐标志!
删除音乐模块
删除单个音乐
请求响应设计
请求:{ post /music/delete id}响应:{ status:0, message:"删除成功" data:true}
代码实现
这里的删除操作需要分成 2 步
将服务器下的文件进行删除
将数据库中的文件信息删除
所以我们需要先查询到该 id 的音乐信息,再进行删除!
Mapper 接口
Mapper 实现
Controller 层代码实现
//删除单个音乐 @RequestMapping("/delete") public ResponseBodyMessage<Boolean> deleteMusic(@RequestParam Integer id){ //1.首先找到该音乐信息 Music music = musicMapper.getMusicById(id); if(music==null){//音乐不存在 //未找到该音乐 return new ResponseBodyMessage<>(-1,"需要删除的音乐不存在",false); } //2.进行音乐删除 //2.1 删除服务器下的音乐文件 //找到服务器下该音乐文件路径 File file = new File(SAVE_PATH+File.separator+music.getTitle()+".mp3"); if(file==null){//服务器下不存在该文件 return new ResponseBodyMessage<>(-1,"删除失败,服务器下未找到该文件信息",false); } //删除 file.delete(); //2.2 删除数据库下的音乐信息 int ret = musicMapper.deleteById(id); if(ret!=1){//数据库删除失败 return new ResponseBodyMessage<>(-1,"数据库信息删除失败",false); } return new ResponseBodyMessage<>(0,"删除成功!",true); }
验证结果
先查看数据库下的音乐信息
删除的音乐不存在
删除音乐存在
删除成功!
批量删除音乐
请求响应设计
请求:{ post /music/deleteAll id[]}响应:{ status:0 message:"删除成功" data:true}
代码实现
我们只需要在删除单个音乐的基础上进行代码的修改即可!
直接增加Controller层代码即可!
//批量删除音乐 @RequestMapping("/deleteAll") public ResponseBodyMessage<Boolean> deleteMusicAll(@RequestParam(value = "id[]") List<Integer> ids){ String message = null; for (Integer id:ids) { //1.首先找到该音乐信息 Music music = musicMapper.getMusicById(id); if(music==null){//音乐不存在 //未找到该音乐 //保存这个音乐id信息 message += id+" "; continue; //return new ResponseBodyMessage<>(-1,"需要删除的音乐不存在",false); } //2.进行音乐删除 //2.1 删除服务器下的音乐文件 //找到服务器下该音乐文件路径 File file = new File(SAVE_PATH+File.separator+music.getTitle()+".mp3"); System.out.println("musicPath:"+file.getPath()); if(file==null){//服务器下不存在该文件 //保存这个id信息 message += id + ""; continue; //return new ResponseBodyMessage<>(-1,"删除失败,服务器下未找到该文件信息",false); } //删除 if(!file.delete()){ //删除失败 message += id + ""; continue; //return new ResponseBodyMessage<>(-1,"服务器删除失败",false); } //2.2 删除数据库下的音乐信息 int ret = musicMapper.deleteById(id); if(ret!=1){//数据库删除失败 message += id+" "; continue; //return new ResponseBodyMessage<>(-1,"数据库信息删除失败",false); } } if(message==null){ return new ResponseBodyMessage<>(0,"删除成功!",true); } //部分删除失败 return new ResponseBodyMessage<>(0,"id:" + message+" 删除失败!",true); }
验证结果
删除成功
删除失败
查询音乐模块
我们的查询需要支持一下功能
查询给定名称的歌曲
给定歌曲名称全
查询到单个歌曲
给定名称字段(支持模糊匹配查询)
查询到多个歌曲
未给定歌曲名
就查询所有歌曲
请求响应设计
请求:{ get /music/findMusic musicName}
响应:{ status:0 message:"查询成功" data: { { id:2 title:"隆里电丝" singer:"大傻" time:"2022年8月1日" url:/music/get?path="隆里电丝" }, .... }}
代码实现
MusicMapper接口新增方法查询所有音乐
模糊匹配查询某些音乐
/** *查询所有歌曲 * @return */ List<Music> findMusic();
/** * 通过名称查询到歌曲信息,支持模糊查询 * @return */ List<Music> findMusicByName(String musicName);
MusicMapper.xml实现
<!--查询所有歌曲--> <select id="findMusic" resultType="com.example.onlinemusic.model.Music"> select * from music; </select> <!--模糊查询指定歌曲--> <select id="findMusicByName" resultType="com.example.onlinemusic.model.Music"> select * from music where title like concat('%',#{musicName},'%') </select>
MusicController实现
//模糊查询 @RequestMapping("/findMusic") public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required =false) String musicName){ List<Music> musicList = new LinkedList<>(); if(musicName==null){ //查询名称为空,查询所有音乐返回 musicList = musicMapper.findMusic(); return new ResponseBodyMessage<>(0,"查询成功!",musicList); } //进行模糊查询! musicList = musicMapper.findMusicByName(musicName); return new ResponseBodyMessage<>(0,"查询成功!",musicList); }
验证结果
查询名称为空
模糊查询
收藏音乐模块
添加音乐到收藏列表
请求响应设计
请求:{ post, /lovemusic/likeMusic data:user_id,music_id}
响应:{ status:0, message:"收藏音乐成功", data:true}
代码实现
我们要将一首音乐收藏分为 2 步
找到该音乐信息,判断是否收藏过(查询
lovemusic表)收藏该音乐(添加到
lovemusic表)
新增
LoveMusicMapper接口
package com.example.onlinemusic.mapper;
import com.example.onlinemusic.model.LoveMusic;import org.apache.ibatis.annotations.Mapper;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-08-01 * Time: 19:54 */@Mapperpublic interface LoveMusicMapper { /** * 通过用户id和音乐id查询喜欢音乐 * @param user_id * @param music_id * @return */ LoveMusic findLoveMusicByUidAndMusicId(int user_id, int music_id);
/** * 添加音乐到收藏列表 * @param user_id * @param music_id * @return */ int insetLoveMusic(int user_id,int music_id);}
LoveMusicMapper.xml实现接口
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.onlinemusic.mapper.LoveMusicMapper"> <!--添加音乐到收藏列表--> <insert id="insetLoveMusic"> insert into lovemusic (user_id,music_id) values(#{user_id},#{music_id}) </insert> <!--通过用户id和音乐id查询收藏列表--> <select id="findLoveMusicByUidAndMusicId" resultType="com.example.onlinemusic.model.LoveMusic"> select * from lovemusic where user_id = #{user_id} and music_id = #{music_id} </select></mapper>
LoveMusicController代码实现
package com.example.onlinemusic.controller;
import com.example.onlinemusic.mapper.LoveMusicMapper;import com.example.onlinemusic.model.Contant;import com.example.onlinemusic.model.LoveMusic;import com.example.onlinemusic.model.User;import com.example.onlinemusic.tools.ResponseBodyMessage;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpSession;
/** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-08-01 * Time: 20:25 */@RestController@RequestMapping("/lovemusic")public class LoveMusicController {
//注入LoveMusicMapper @Resource private LoveMusicMapper loveMusicMapper;
//收藏音乐 @RequestMapping("/likeMusic") public ResponseBodyMessage<Boolean> insertLoveMusic(@RequestParam Integer id,HttpServletRequest request){ //1.检查登入状态,未登入不创建回话 HttpSession session = request.getSession(false); if(session==null||null==request.getSession().getAttribute(Contant.USERINFO_SESSION_KEY)){ //未登入状态 return new ResponseBodyMessage<>(-1,"请登入用户",false); } //登入状态,进行音乐收藏 //1.获取到用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); System.out.println("user_id:"+user_id+" music_id:"+id); //2.查询该歌曲是否已存在收藏列表 LoveMusic loveMusic = loveMusicMapper.findLoveMusicByUidAndMusicId(user_id,id); if(loveMusic!=null){ //该歌曲已收藏! System.out.println("lovemusic:"+loveMusic); return new ResponseBodyMessage<>(-1,"收藏失败,该歌曲收藏",false); } //未收藏,将其收藏! int flg = loveMusicMapper.insetLoveMusic(user_id,id); if(flg!=1){ return new ResponseBodyMessage<>(-1,"收藏失败",false); } return new ResponseBodyMessage<>(0,"收藏成功!",true); }}
验证结果
歌曲已收藏,收藏失败
歌曲未收藏,收藏成功
查询喜欢的音乐列表
这里的查询喜欢列表和查询音乐模块类似!
未给定歌曲名称
查询该用户所有收藏歌曲
给定歌曲名称参数
查询歌曲名称含有该参数的歌曲
请求和响应设计
请求:{ get /lovemusic/findloveMusic data:{musicName:musicName}}
响应:{ status:0, message:"查询到收藏的音乐", data: { { id:1, title:"隆里电丝", singer:"大傻", time:"2022年8月2日", url:"/music/get?path=隆里电丝", user_id:2 } ... }}
代码实现
LoveMusicMapper接口新增方法
/** * 查询该用户所有的收藏歌曲 * @param user_id * @return */ List<Music> findLoveMusic(int user_id);
/** * 通过用户id和歌曲名称查询收藏歌曲支持模糊匹配 * @param user_id * @param musicName * @return */ List<Music> findLoveMusicByUidAndMusicName(int user_id,String musicName);
xml实现接口方法
<!--在该用户id下通过歌曲名称查询歌曲(支持模糊查询)--> <select id="findLoveMusicByUidAndMusicName" resultType="com.example.onlinemusic.model.Music"> select m.* from music as m, lovemusic as lm where m.id = lm.music_id and lm.user_id = #{user_id} and m.title like concat('%',#{musicName},'%') </select> <!--通过id查询收藏列表--> <select id="findLoveMusicById" resultType="com.example.onlinemusic.model.LoveMusic"> select * from lovemusic where id = #{id} </select>
LoveMusicController代码实现
//通过音乐名称查询收藏列表 @RequestMapping("/findloveMusic") public ResponseBodyMessage<List<Music>> findLoveMusicByUidAndMusicName(@RequestParam(required = false) String musicName, HttpServletRequest request){ //1.检查登入状态,未登入不创建回话 HttpSession session = request.getSession(false); if(session==null||null==request.getSession().getAttribute(Contant.USERINFO_SESSION_KEY)){ //未登入状态 return new ResponseBodyMessage<>(-1,"请登入用户",null); } //登入状态,进行音乐查询 //1.获取到用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); System.out.println("user_id:"+user_id+" musicName:"+musicName); //2.查询收藏列表 List<Music> musicList = null; if(musicName==null){ //2.1歌曲名称为空,查询该用户所有收藏歌曲! musicList = loveMusicMapper.findLoveMusic(user_id); return new ResponseBodyMessage<>(0,"查询到收藏列表",musicList); } //2.2 歌曲名称不为空,模糊查询 musicList = loveMusicMapper.findLoveMusicByUidAndMusicName(user_id,musicName); return new ResponseBodyMessage<>(0,"查询收藏列表成功",musicList); }
验证结果
数据库信息:
登入user_di=10的用户
查询该用户所有收藏歌曲
模糊匹配查询指定歌曲
从喜欢列表移除音乐
请求和响应设计
请求:{ get, /lovemusic/deleteloveMusic, data:{id}}响应:{ status:0, message:"取消收藏成功", data:true}
代码实现
LoveMusicMapper接口新增方法
/** * 通过用户id和音乐id取消音乐收藏 * @param user_id * @param music_id * @return */ int deleteLoveMusicByUidAndMusicId(int user_id, int music_id);
/** * 通过音乐id删除lovemusic表中的信息 * @param music_id * @return */ int deleteLoveMusicByMusicId(int music_id);
xml实现接口
<!--通过用户id和音乐id移除收藏歌曲--> <delete id="deleteLoveMusicByUidAndMusicId"> delete from lovemusic where user_id = #{user_id} and music_id = #{music_id} </delete> <!--通过musicId移除收藏列表--> <delete id="deleteLoveMusicByMusicId"> delete from lovemusic where music_id = #{music_id} </delete>
LoveMusicMapperController代码实现
//通过收藏表中的id取消收藏歌曲信息 @RequestMapping("/deleteloveMusic") public ResponseBodyMessage<Boolean> deleteLoveMusic(@RequestParam Integer id,HttpServletRequest request){ //1.检查登入状态,未登入不创建回话 HttpSession session = request.getSession(false); if(session==null||null==request.getSession().getAttribute(Contant.USERINFO_SESSION_KEY)){ //未登入状态 return new ResponseBodyMessage<>(-1,"请登入用户",false); } //登入状态,可进行取消音乐收藏功能 //1.获取到用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); //取消收藏! int ret = loveMusicMapper.deleteLoveMusicByUidAndMusicId(user_id,id); if(ret!=1){ return new ResponseBodyMessage<>(-1,"取消收藏失败",false); } return new ResponseBodyMessage<>(0,"取消收藏成功",true); }
验证结果
数据库信息:
歌曲不存在,移除失败
歌曲存在,取消收藏成功
完善删除音乐模块代码
我们对项目增添了收藏列表后,发现一个问题!我们上传的音乐删除后,收藏的音乐就不存在了,那么收藏列表中关于这首歌曲的信息也要删除,所以我们对我们的删除音乐代码进行完善!
删除音乐时,要先检查该音乐是否在收藏列表中,如果在就将歌曲移除收藏列表
我们只需要添加一个通过music_id取消收藏音乐的方法即可
LoveMusicMapper新增方法
/** * 通过音乐id删除lovemusic表中的信息 * @param music_id * @return */ int deleteLoveMusicByMusicId(int music_id);
xml实现
<!--通过musicId移除收藏列表--> <delete id="deleteLoveMusicByMusicId"> delete from lovemusic where music_id = #{music_id} </delete>
//删除单个音乐 @RequestMapping("/delete") public ResponseBodyMessage<Boolean> deleteMusic(@RequestParam Integer id){ //1.首先找到该音乐信息 Music music = musicMapper.getMusicById(id); if(music==null){//音乐不存在 //未找到该音乐 return new ResponseBodyMessage<>(-1,"需要删除的音乐不存在",false); } //2.进行音乐删除 //2.1 删除服务器下的音乐文件 //找到服务器下该音乐文件路径 File file = new File(SAVE_PATH+File.separator+music.getTitle()+".mp3"); System.out.println("musicPath:"+file.getPath()); if(file==null){//服务器下不存在该文件 return new ResponseBodyMessage<>(-1,"删除失败,服务器下未找到该文件信息",false); } //删除 if(!file.delete()){ //删除失败 return new ResponseBodyMessage<>(-1,"服务器删除失败",false); } //2.2 删除数据库下的音乐信息 //2.2.1删除music表中的音乐信息 int ret = musicMapper.deleteById(id); if(ret!=1){//数据库删除失败 return new ResponseBodyMessage<>(-1,"数据库信息删除失败",false); } //2.2.2删除lovemusic表中的音乐信息 loveMusicMapper.deleteLoveMusicByMusicId(id); return new ResponseBodyMessage<>(0,"删除成功!",true); }
//批量删除音乐 @RequestMapping("/deleteAll") public ResponseBodyMessage<Boolean> deleteMusicAll(@RequestParam(value = "id[]") List<Integer> ids){ String message = ""; for (Integer id:ids) { //1.首先找到该音乐信息 Music music = musicMapper.getMusicById(id); if(music==null){//音乐不存在 //未找到该音乐 //保存这个音乐id信息 message += id+" "; continue; //return new ResponseBodyMessage<>(-1,"需要删除的音乐不存在",false); } //2.进行音乐删除 //2.1 删除服务器下的音乐文件 //找到服务器下该音乐文件路径 File file = new File(SAVE_PATH+File.separator+music.getTitle()+".mp3"); System.out.println("musicPath:"+file.getPath()); if(file==null){//服务器下不存在该文件 //保存这个id信息 message += id + ""; continue; //return new ResponseBodyMessage<>(-1,"删除失败,服务器下未找到该文件信息",false); } //删除 if(!file.delete()){ //删除失败 message += id + ""; continue; //return new ResponseBodyMessage<>(-1,"服务器删除失败",false); } //2.2 删除数据库下的音乐信息 int ret = musicMapper.deleteById(id); if(ret!=1){//数据库删除失败 message += id+" "; continue; //return new ResponseBodyMessage<>(-1,"数据库信息删除失败",false); } //取消该歌曲的所有收藏 loveMusicMapper.deleteLoveMusicByMusicId(id); } if(message==""){ return new ResponseBodyMessage<>(0,"删除成功!",true); } //部分删除失败 return new ResponseBodyMessage<>(0,"id:" + message+" 删除失败!",true); }
验证结果:
数据库信息:
我们将music_id为 51 音乐删除!
前端设计
我们前端页面是在网上下载了一个静态页面的模板!我们需要在此基础上进行js代码的编写,从而实现前后端用户的接口
登入逻辑
核心逻辑:
<script> //核心业务逻辑 ///$(function () {} 当DOM加载完毕后执行 $(function(){ //当点击这个按钮后就会执行function函数 $("#submit").click(function(){ //获取到用户名和密码 var username = $("#user").val(); var password = $("#password").val(); //判断用户名密码是否为空! if(username.trim()==""||password.trim()==""){ alert("请输入用户名和密码"); return; } //这里就需要将请求前端数据返回给服务器! $.ajax({ url:"/user/login",//指定路径 type:"post", data:{"username":username,"password":password}, dataType:"json", //设置服务器返回json数据 success:function(body){//回调函数 console.log(body); if(body.status==0){//通过后端发送回来的status判断登入状态! alert("登入成功!"); //登入成功后跳转到指定页面 window.location.href("list.html"); }else{ alert("登入失败,账号或密码错误,请重试!"); //将输入框清空! $("#message").text(""); $("#user").val(""); $("#password").val(""); } } }); }); });</script>
验证结果:
注册逻辑
<script> //核心业务逻辑 ///$(function () {} 当DOM加载完毕后执行 $(function(){ //当点击这个按钮后就会执行function函数 $("#submit").click(function(){ //获取到用户名和密码 var username = $("#user").val(); var password = $("#password").val(); //判断用户名密码是否为空! if(username.trim()==""||password.trim()==""){ alert("请输入用户名和密码"); return; } //这里就需要将请求前端数据返回给服务器! $.ajax({ url:"/user/login",//指定路径 type:"post", data:{"username":username,"password":password}, dataType:"json", //设置服务器返回json数据 success:function(body){//回调函数 console.log(body); if(body.status==0){//通过后端发送回来的status判断登入状态! alert("登入成功!"); //登入成功后跳转到指定页面 window.location.href = "list.html"; }else{ alert("登入失败,账号或密码错误,请重试!"); //将输入框清空! $("#message").text(""); $("#user").val(""); $("#password").val(""); } } }); }); }); </script>
验证结果:
'
查询音乐逻辑
核心代码逻辑:
<script type="text/javascript"> $(function(){ load(); }); //musicName可以传参,也可以不传 function load(musicName){ $.ajax({ url:"/music/findMusic",//指定路径 type:"get", data:{"musicName":musicName}, dataType:"json", //设置服务器返回json数据 success:function(body){//回调函数 console.log(body); var s = ''; var data = body.data;//数组! for(var i = 0;i<data.length;i++){ var musicUrl = data[i].url+'.mp3'; s += '<tr>'; s += '<th> <input id="'+data[i].id+'"type="checkbox"> </th>'; s += '<td>' + data[i].title + '</td>'; s += '<td>' + data[i].singer + '</td>'; s +='<td > <button class="btn btn-primary" onclick="playerSong(\''+musicUrl+'\')" >播放歌曲</button>' + '</td>'; s +='<td > <button class="btn btn-primary" onclick="deleteInfo('+ data[i].id + ')" >删除</button>' + ' '+'<button class="btn btn-primary" onclick="loveInfo('+ data[i].id + ')" > 喜欢</button>'+ '</td>'; s += '</tr>'; } $("#info").html(s);//把拼接好的页面放在info的id下 } }); } </script>
验证结果:
上传音乐逻辑
核心代码逻辑
<form method="post" enctype="multipart/form-data" action="/music/upload"> 文件上传:<input type="file" name="filename"/> 歌手名: <label> <input type="text" name="singer" placeholder="请输入歌手名"/> </label> <input type="submit" value="上传"/></form>
验证结果:
播放音乐
<!--嵌入播放器!--><div style="width: 180px; height: 140px; position:absolute; bottom:10px; right:10px"> <script type="text/javascript" src="player/sewise.player.min.js"></script> <script type="text/javascript"> SewisePlayer.setup({ server: "vod", type: "mp3", //播放的地址 videourl:"http://jackzhang1204.github.io/materials/where_did_time_go.mp3", //皮肤! skin: "vodWhite", //这里自动播放需要设置false autostart:"false", }); </script> </div>
//播放音乐 function playerSong(obj) { console.log(obj) var name = obj.substring(obj.lastIndexOf('=')+1); //obj:播放地址 name:歌曲或者视频名称 0:播放开始时间 false:点击后自动播放 SewisePlayer.toPlay(obj,name,0,true); }
验证结果:
删除音乐逻辑
//删除音乐 function deleteInfo(obj){ console.log(obj); $.ajax({ url:"/music/delete", type:'post', dataType:"json", data:{"id":obj}, success:function(body){ console.log(body); if(body.data==true){ //删除成功! alert("删除成功!"); window.location.href = "list.html"; }else{ alert("删除失败!"); } } }); }
验证结果:
查询歌曲逻辑
$(function(){ $('#submit1').click(function(){ var name = $("#exampleInputName2").val(); load(name); }); });
验证结果:
删除选中的歌曲逻辑
//删除选中逻辑在查询逻辑里 $(function(){ $('#submit1').click(function(){ var name = $("#exampleInputName2").val(); load(name); }); //当查询结束才可进行选中删除(有了音乐列表) $.when(load).done(function(){ //选中删除逻辑! $("#delete").click(function(){ var id = new Array(); var i = 0;//数组下标! //遍历input标签下的checkbox $("input:checkbox").each(function(){ //判断是否选中! if($(this).is(":checked")){ //获取input里面的id值! id[i] = $(this).attr('id'); i++; } }); console.log(id); $.ajax({//将获取到的id发送给服务器! url:"/music/deleteAll", data:{"id":id}, type:'post',
success:function(body){ if(body.status==0){ alert("删除成功!"); window.location.href="list.html"; }else{ alert("删除失败!"); } } }); }); }); });
验证结果:
收藏音乐逻辑
$(function(){ load(); }); //musicName可以传参,也可以不传 function load(musicName){ $.ajax({ url:"/lovemusic/findLoveMusic",//指定路径 type:"get", data:{"musicName":musicName}, dataType:"json", //设置服务器返回json数据 success:function(body){//回调函数 console.log(body); var s = ''; var data = body.data;//数组! for(var i = 0;i<data.length;i++){ var musicUrl = data[i].url+'.mp3'; s += '<tr>'; s += '<td>' + data[i].title + '</td>'; s += '<td>' + data[i].singer + '</td>'; s +='<td > <button class="btn btn-primary" onclick="playerSong(\''+musicUrl+'\')" >播放歌曲</button>' + '</td>'; s +='<td > <button class="btn btn-primary" onclick="deleteInfo('+ data[i].id + ')" >移除</button>' + '</td>'; s += '</tr>'; } $("#info").html(s);//把拼接好的页面放在info的id下 } }); } //播放音乐 function playerSong(obj) { console.log(obj) var name = obj.substring(obj.lastIndexOf('=')+1); //obj:播放地址 name:歌曲或者视频名称 0:播放开始时间 false:点击后自动播放 SewisePlayer.toPlay(obj,name,0,true); } //删除音乐 function deleteInfo(obj){ console.log(obj); $.ajax({ url:"/lovemusic/deleteloveMusic", type:'post', dataType:"json", data:{"id":obj}, success:function(body){ console.log(body); if(body.data==true){ //删除成功! alert("移除成功!"); window.location.href = "loveMusic.html"; }else{ alert("移除失败!"); } } }); }
验证结果:
实现收藏功能逻辑
//在list.html文件中添加一个收藏歌曲函数即可!//收藏歌曲 function loveInfo(obj){ $.ajax({ url:"/lovemusic/likeMusic", type:"post", data:{"id":obj}, dataType:"json", success:function(body){ if(body.data==true){ alert("收藏成功!"); window.location.href="list.html"; }else{ alert("收藏失败!"); } } }) }
验证结果:
配置拦截器
有些页面我们没有登入也可以进行访问,通过输入
url到地址栏即可!
我们可以在项目中进行登入状态的检查,如果登入了就可以访问,否则不能,这就配置了拦截器,保证程序安全!
我们首先在
config包下自定义一个拦截器
核心代码
package com.example.onlinemusic.config;
import com.example.onlinemusic.model.Contant;import com.example.onlinemusic.tools.ResponseBodyMessage;import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;
/** * Created with IntelliJ IDEA. * Description:配置拦截器 * User: hold on * Date: 2022-08-06 * Time: 0:43 */public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //检查是否登入! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return false; } return true; }}
然后将配置好的拦截器添加到
AppConfig类中
package com.example.onlinemusic.config;
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/** * Created with IntelliJ IDEA. * Description:将自定义拦截器添加到系统配置! * User: hold on * Date: 2022-07-27 * Time: 12:29 */@Configurationpublic class AppConfig implements WebMvcConfigurer { //将BCrypetPasswordEncoder对象交给spring管理! @Bean public BCryptPasswordEncoder getBCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); }
//添加拦截器! @Override public void addInterceptors(InterceptorRegistry registry) { LoginInterceptor loginInterceptor = new LoginInterceptor(); registry.addInterceptor(loginInterceptor) //拦截该项目目录下的所有文件! .addPathPatterns("/**") //排除无关文件 .excludePathPatterns("/js/**.js") .excludePathPatterns("/images/**") .excludePathPatterns("/css/**.css") .excludePathPatterns("/fronts/**") .excludePathPatterns("/player/**") //排除登入注册接口! .excludePathPatterns("/login.html") .excludePathPatterns("/user/login") .excludePathPatterns("/reg.html") .excludePathPatterns("/user/register"); }}
验证结果:
部署到服务器
准备
更改数据库配置
因为我的云服务器下的数据库用户密码信息和本地的一样,所以不用更改数据库配置!
我们在云服务器下创建好数据库即可!
更改上传音乐路径
# 云服务器下的地址路径!music.path.save = /root/javaweb部署环境/music
打包
项目打包
打包报错,我们需要统一编码 UTF-8
添加依赖
<!--添加maven.plugins依赖 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.1.0</version> </plugin>
<!--添加maven-surefire-plugin依赖--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin>
打包成功!
部署
将打包好的项目上传到服务器!
启动项目:
java -jar onlinemusic.jar
这里显示端口号8080被占用,那么我们就将端口号给kill
//查看端口netstat -anp | grep 8080//杀进程kill -9 pcb值
验证结果:
这样虽然部署好了项目,但是这个只能支持前台运行,也就是说这个项目关闭了,项目就访问不了了!
后台运行 SpringBoot 项目
运行指令:
nohup java -jar onlinemusic.jar>>log.log&
nohup:后台运行项目的指令
>>log.log:把控制台上的日志保存到 log.log 文件中!(未设置可默认生成)
&:后台一直运行
这样运行就支持后台运行了!
后期项目维护更新
如果后面我们觉得项目需要完善该如何进行服务器项目更新呢?
将该项目的进制终止
netstat -anp |grep 8080
kill -9 17303
也可以使用ps -ef | grep java
kill 【进程ID】命令说明:ps : Linux 当中查看进程的命令-e 代表显示所有的进程-f 代表全格式【显示全部的信息】grep : 全局正则表达式重新上传jar包重新进行后台的启动
更新项目,重新运行
nohup java -jar onlinemusic.jar>>log.log&
更新了一下注册登入的前端页面!
遇到的面试题总结
上传其他文件,然后将后缀改成
.mp3,如何识别?是否可以正常播放?
因为每种类型的文件都有自己的文件结构,都有自己特有的格式,我们根据
mp3特有的文件格式,在倒数第128字节处,有有个TAG音乐文件标志,从而在上传时就检测一下是否是音频文件,如果不是音频文件无法上传!
可以上传大文件嘛?
不能,因为一首歌曲的大小不会很大,所以我已经在配置文件配置了每个文件的最大上传大小,以及单次请求的文件总数大小!
#配置springboot上传文件的大小,默认每个文件的配置最大为15Mb,单次请求的文件的总数不能大于100Mb spring.servlet.multipart.max-file-size = 15MB spring.servlet.multipart.max-request-size=100MB
为啥不用
HTML的原生audio标签?
因为我想通过使用开源的播放器,提升一下自己的学习能力,毕竟我们经常会在自己的项目中使用到其他的优秀开源项目,我们也需要具备这样的能力,学习使用大佬的优秀项目!
只要将开源播放代码换成原生
audio即可!s += "<td <a href=\"\"> <audio src= \""+ musicUrl+"\" + controls=\"controls\" preload=\"none\" loop=\"loop\"> >" + "</audio> </a> </td>";![]()
原生的
audio标签和开源播放器的一首歌曲的下载时间如下:开源播放器:
![]()
原生
audio播放器:![]()
可以看到同样一首歌曲在线播放后下载的时间不同,虽然 2 个都是边下载边播放,但是这里的开源播放器下载时间更短!
版权声明: 本文为 InfoQ 作者【bug郭】的原创文章。
原文链接:【http://xie.infoq.cn/article/2c576c5ef890e9e9c47b3b089】。文章转载请联系作者。
bug郭
还未添加个人签名 2022.06.15 加入
还未添加个人简介










评论