1. 项目简介
本项目主要是实现了在线播放音乐的功能,用户可以随时登陆听自己喜欢的音乐
项目实现的业务功能
- 注册功能:用户输入账号,密码,并且确认密码后可以完成注册
- 登陆功能:用户输入注册成功的账号密码,可以登陆
- 添加音乐:用户登陆成功后,在主页可以上传音乐
- 查询音乐:用户可以输入音乐名称或者关键字查询相关的音乐
- 单个删除:用户可以点击按钮,删除对应的音乐
- 批量删除:用户一次可以删除多个音乐
- 收藏功能:用户点击对应的按钮,即可收藏对应的音乐
- 取消收藏:用户可以在收藏列表点击取消收藏按钮,对应的音乐即被取消收藏
- 播放功能:用户可以点击每首音乐的播放按钮,即可实时播放音乐
- 注销功能:用户点击此按钮,退出登陆
项目功能展示
注册:
登陆:
音乐列表:
查询功能:
收藏列表:
收藏列表的查询功能:
我已经将项目部署到到服务器上了,感兴趣的小伙伴可以访问以下链接:个人在线音乐播放器
2. 数据库表的设计
通过上面的业务功能可知,需要一张用户表保存用户信息,需要一张音乐表保存用户添加的音乐信息,还需要一张收藏表来保存每个用户收藏的音乐
用户表:只保存用户基本信息即主键,用户名,密码
create table user(
id int primary key auto_increment,
username varchar(20) not null,
password varchar(255) not null
);
音乐表:音乐表有主键,音乐名称,歌手,上传时间,因为要播放音乐,所以此处还需要保存音乐地址的信息,多个用户都可以上传音乐,所以还需要一个用户id字段
create table music(
id int primary key auto_increment,
title varchar(20) not null,
singer varchar(20) not null,
`time` varchar(13) not null,
url varchar(100) not null,
user_id int not null
);
收藏表:此表将用户和喜欢的音乐关联起来,所以字段为主键,用户id,音乐id
create table lovemusic(
id int primary key auto_increment,
user_id int not null,
music_id int not null
);
3. 拦截器及返回数据格式
创建拦截器对验证用户是否登陆功能做统一处理
创建拦截器:创建一个类实现HandlerInterceptor接口重写preHandle方法
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if(session == null || session.getAttribute("user") == null){
return false;
}
return true;
}
}
添加拦截器:创建一个类实现WebMvcConfigurer接口,重写addInterceptors方法添加拦截规则
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LoginInterceptor loginInterceptor = new LoginInterceptor();
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").
excludePathPatterns("/login.html").
excludePathPatterns("/register.html").
excludePathPatterns("/user/register").
excludePathPatterns("/user/login").
excludePathPatterns("/css/**.css").
excludePathPatterns("/fonts/**").
excludePathPatterns("/images/**").
excludePathPatterns("/js/**.js").
excludePathPatterns("/player/**");
}
}
注意:登陆页面,注册页面和所有的静态样式都要开放出来
对返回前端的数据进行统一封装
返回给前端的数据应该包含状态码,提示信息,数据
@Data
public class ResponseMessage {
private int status;
private String message;
private Object data;
public ResponseMessage(int status, String message, Object data) {
this.status = status;
this.message = message;
this.data = data;
}
}
4. 注册功能
前端设计
对于注册功能,用户输入用户名,密码,并且必须确认密码之后才可注册,所以在前端就对输入内容进行了判空操作,也对确认密码进行了校验,获取到输入框的用户名和密码,发送POST请求,待后端返回数据后,如果注册成功,跳转到登陆页面,如果注册参数有误,则清空输入框,让用户重新注册
后端设计
前端传来的参数有用户名,密码,首先判断用户名是否存在,根据用户名在用户表中查询,看能否查询出信息,如果查询到了,说明该账号已被注册过了,如果查询不到信息,说名该账号没有被注册,可以添加用户名和密码到数据库中了
//注册
@RequestMapping("/register")
public ResponseMessage register(@RequestParam String username,@RequestParam String password){
//判断用户名是否存在
User user = userService.getUserByName(username);
if(user != null){
//用户名存在,直接返回错误信息给前端
return new ResponseMessage(-1,"用户名已存在",false);
}
//用户名不存在,往用户表插入注册数据
int n = userService.insertUser(username,password);
if(n == 1){
return new ResponseMessage(1,"注册成功",true);
}else {
return new ResponseMessage(-1,"注册失败",false);
}
}
查询和插入sql:
<select id="getUserByName" resultType="com.example.demo.model.User">
select * from user where username=#{username}
</select>
<insert id="insertUser">
insert into user (username,password) values(#{username},#{password})
</insert>
5. 登陆功能
前端设计
获取到输入框的用户名和密码后,对数据进行判空后,然后发送POST请求,待后端返回响应后,如果登陆成功,则跳转到音乐列表页面,如果用户名或密码不正确,提示用户错误信息
后端设计
前端传来的参数有用户名,密码,首先校验用户名是否存在,如果用户名不存在返回给前端错误信息,如果用户名存在,再校验密码是否正确,如果正确,创建session保存用户信息
//登陆
@RequestMapping("/login")
public ResponseMessage login(@RequestParam String username, @RequestParam String password, HttpServletRequest req){
//校验用户是否存在
User user = userService.getUserByName(username);
if(user == null){
return new ResponseMessage(-1,"用户名不存在","");
}else {
//校验密码是否正确
if(!password.equals(user.getPassword())){
return new ResponseMessage(-1,"密码错误","");
}
//创建session保存用户信息
HttpSession session = req.getSession(true);
session.setAttribute("user",user);
return new ResponseMessage(1,"登陆成功",user);
}
}
查询sql:
<select id="getUserByName" resultType="com.example.demo.model.User">
select * from user where username=#{username}
</select>
6. 音乐列表相关业务
6.1 查询功能
前端设计
页面一加载就要查询该用户所有的音乐,并且用户也可以手动的输入音乐昵称进行模糊查询,所以前端创建函数的时候,该函数的参数可以为空,也可以是音乐名称,当参数为空的时候就要在页面加载完成后立即调用,当参数不为空的时候,就要由用户手动点击按钮进行调用
后端设计
当不输入任何名称时候,默认查找所有音乐,也就是当访问到该页面时自动进行了不带任何名称的查询,所以一加载到页面就能看到全部的音乐信息,当输入名称时,根据输入的名称模糊匹配,在进行查询的时候默认带上用户id,因为数据库中的音乐是多个用户拥有的
//根据歌名查询音乐,当歌名为空时,查询全部音乐(根据歌名和用户id查询)
@RequestMapping("/findmusic")
public ResponseMessage findMusic(String musicName,HttpServletRequest req){
//获取到用户id,根据用户id查询该用户的音乐
HttpSession session = req.getSession();
User user = (User)session.getAttribute("user");
int userId = user.getId();
//当查询内容为空时,查询该用户所有音乐
if(musicName == null){
List<Music> allMusics = musicService.getAllMusic(userId);
return new ResponseMessage(1,"查询成功",allMusics);
}
//当查询内容不为空时,根据查询内容模糊匹配
List<Music> musics = musicService.findByName(musicName,userId);
return new ResponseMessage(1,"查询成功",musics);
}
查询sql:
<select id="getAllMusic" resultType="com.example.demo.model.Music">
select * from music where user_id=#{userId}
</select>
<select id="findByName" resultType="com.example.demo.model.Music">
select * from music where title like concat('%',#{musicName},'%') and user_id=#{userId}
</select>
6.2 上传音乐
前端设计
上传的音乐包含音乐文件和歌手,所以前端要对参数进行判空校验,参数校验完后,就发送body格式为FormData格式的数据,因为文件上传需要使用改格式,待后端返回响应后,如果上传成功就跳转到音乐列表页面,如果上传失败就提示用户错误信息
后端设计
- 校验该音乐是否存在
上传音乐的参数有音乐文件和歌手,后端接收文件的类型为MultipartFile,需要注意的是必须添加@RequestPart
注解,在获取到音乐文件后,首先要判断该音乐存在不存在,这里我们认为歌手和音乐文件的名称相同的音乐是同一首音乐,所以得先获取上传文件的名称,根据这个名称和歌手在数据中校验该音乐是否存在,由于同一首音乐可以被多个用户上传,所以此处还必须带上用户id这个参数,当该音乐存在时,返回前端错误信息,当不存在时在执行后边逻辑
HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
int userId = user.getId();
//检查数据库是否已经有了该音乐(音乐名称+歌手)
String fileName = file.getOriginalFilename(); //获取文件名称,xxx.mp3
String title = fileName.substring(0,fileName.lastIndexOf(".")); //获取音乐名称
Music getMusic = musicService.getMusic(title,singer,userId);
if(getMusic != null){
return new ResponseMessage(-1,"此音乐已经存在",false);
}
查询sql:
<select id="getMusic" resultType="com.example.demo.model.Music">
select * from music where title=#{title} and singer=#{singer} and user_id=#{userId}
</select>
- 保存文件到服务器本地
如果该音乐不存在,先保存音乐到服务器,此时我的电脑就是服务器,所以保存在本地磁盘,如果部署到linux上,linux就是服务器主机,需要保存到linux,在保存的时候如果出现异常,就保存失败,返回错误信息给前端
注意:保存文件的时候,应该给每个用户有一个单独的文件来保存音乐,保证每个用户在删除音乐文件时,别的用户的音乐文件不受干扰
//保存文件到服务器
//SAVE_PATH为配置的本机路径
String path = SAVE_PATH+userId+"/"+fileName;
File dest = new File(path);
//目录不存在,则创建目录
if(!dest.exists()){
dest.mkdirs();
}
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
return new ResponseMessage(-1,"上传失败",false);
}
- 插入数据库
将文件保存在本地后,就要将该音乐保存到数据中中,此时需要先获取插入需要的数据,因为用户id,歌手,音乐名称在前面都获取过了,所以此处只需获取时间和url,此处的url为播放音乐时的路径
//保存音乐的url,该url为播放音乐时的后端路径
String url = "/music/get?path="+fileName;
//获取上传的时间
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String time = df.format(new Date());
在获取到所有字段的信息后,就将这些数据组装到Music对象中,往数据库中插入这个对象
Music music = new Music();
music.setSinger(singer);
music.setTitle(title);
music.setTime(time);
music.setUserId(user.getId());
music.setUrl(url);
int n = musicService.insertMusic(music);
if(n == 1){
return new ResponseMessage(1,"上传成功",true);
}else {
dest.delete();
return new ResponseMessage(-1,"上传失败",false);
}
插入sql:
<insert id="insertMusic">
insert into music (title,singer,time,url,user_id) values(#{title},#{singer},#{time},#{url},#{userId})
</insert>
6.3 播放音乐
对于播放音乐功能,用户点击播放按钮,后端应该返回给前端音乐文件的内容,我们可以使用ResponseEntity<byte[]>,将文件的内容读到字节数组中,再返回给前端,我们根据前端请求的参数和本地的文件名可以获取到本地保存音乐的文件,使用Files.readAllBytes(file.toPath())
将文件内容读到字节数组中返回给前端
//播放音乐
@RequestMapping("/get")
public ResponseEntity<byte[]> get(String path,HttpServletRequest req){
//返回值为body内容
HttpSession session = req.getSession();
User u = (User) session.getAttribute("user");
int userId = u.getId();
//获取文件路径,需要用户id,因为每个用户的音乐文件路径相互独立
File file = new File(SAVE_PATH+userId+"/"+path);
byte[] bytes = null; //将文件的内容读到字节数组中
try {
bytes = Files.readAllBytes(file.toPath());
if(bytes != null){
return ResponseEntity.ok(bytes); //返回body内容和ok状态
}
} catch (IOException e) {
e.printStackTrace();
}
return ResponseEntity.badRequest().build(); //400
}
6.4 收藏音乐
前端设计
点击某个音乐的收藏按钮,就会携带该音乐的id发送请求,后端接受到音乐id后,将该音乐保存在收藏表中,待后端返回响应,提示成功或失败信息
后端设计
对于收藏功能来说,需要知道收藏的音乐,以及是哪个用户,即建立音乐id和用户id之间对应的关系,但是在往收藏表中添加数据时,需要校验该用户是否已经收藏过该音乐,如果收藏过了就无需再次收藏,如果没有收藏,则插入数据
//收藏音乐
@RequestMapping("/likemusic")
public ResponseMessage likeMusic(String id, HttpServletRequest req){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
//获取到音乐id和用户id
int musicId = Integer.parseInt(id);
int userId = user.getId();
//查看该用户是否收藏过该音乐
LoveMusic loveMusic = loveMusicService.getLoveMusic(userId,musicId);
if(loveMusic != null){
//收藏过,返回错误信息
return new ResponseMessage(-1,"该音乐已被收藏",false);
}
//未收藏过,往收藏表中插入数据
int n = loveMusicService.insertLoveMusic(userId,musicId);
if(n == 1){
return new ResponseMessage(1,"收藏成功",true);
}else {
return new ResponseMessage(-1,"收藏失败",false);
}
}
查询与插入sql:
<select id="getLoveMusic" resultType="com.example.demo.model.LoveMusic">
select * from lovemusic where user_id=#{userId} and music_id=#{musicId}
</select>
<insert id="insertLoveMusic">
insert into lovemusic (user_id,music_id) values (#{userId},#{musicId})
</insert>
6.5 删除功能
删除功能提供了两种,一是每次只删除一个音乐,二是可以选中多个音乐,一次性删除,但是删除时都需要校验该音乐是否存在,因为只有音乐存在才能删除成功
注意: 删除时需要注意一个问题,就是删除音乐时如果用户收藏了这个音乐,那么收藏表中的数据也因该被删除
6.5.1 删除单个音乐
前端设计
点击某个音乐对应的删除按钮,携带音乐id发送请求,后端会将该音乐的数据库中数据和本地保存的音乐文件删掉,待后端返回响应,如果删除成功书信音乐列表页面,如果删除失败提示错误信息
后端设计
前端点击删除,传递给后端的参数是音乐id,后端需要从session中获取到用户id,因为用户点击删除按钮,删除的是自己拥有的音乐,所以需要结合用户id删除,否则就会把其他用户拥有的音乐误删,删除完数据库中数据后,还需要删除本地保存的音乐文件,每个用户都有自己的文件保存路径,所以删除本地路径的时候,删除用户对应路径的音乐文件
//删除单个音乐
@RequestMapping("/delete")
public ResponseMessage deleteById(String id,HttpServletRequest req){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
//获取到用户id
int userId = user.getId();
//获取音乐id
Integer musicId = Integer.parseInt(id);
//校验数据库是否存在要删除的音乐
Music music = musicService.getById(musicId,userId);
if(music == null){
//如果不存在返回给前端错误信息
return new ResponseMessage(-1,"该音乐不存在",false);
}
//如果存在则删除数据库中音乐
int n = musicService.deleteById(musicId,userId);
//删除服务器路径保存的音乐
String path = music.getTitle(); //获取音乐名称
String url = music.getUrl();
String suffix = url.substring(url.lastIndexOf(".")); //获取后缀
path = SAVE_PATH+userId+"/"+path+suffix; //拼接路径
File file = new File(path);
if(file.delete()){
//同步查看收藏的音乐是否有该音乐,如果有也要删除收藏的该音乐
int m = loveMusicService.deleteLoveMusicByMusicId(musicId,userId);
return new ResponseMessage(1,"删除成功",true);
}else {
return new ResponseMessage(-1,"删除失败",false);
}
}
6.5.2 批量删除音乐
前端设计
与上述删除一个音乐类似,只是请求携带的数据是音乐id数组,该数组通过遍历复选框得到,如果遍历的复选框被选中则将此对应的音乐id添加到数组中
后端设计
批量删除的逻辑与上面删除一个的逻辑相同,只是前端传递的是一个音乐id数组,遍历该数组执行与上述相同的逻辑即可
//批量删除
@RequestMapping("/deletepart")
public ResponseMessage deletePart(@RequestParam("id[]") List<Integer> ids,HttpServletRequest req){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
int userId = user.getId();
int sum = 0;
//拿到每一个id,先查再删
for(int i = 0;i < ids.size();i++){
int musicId = ids.get(i);
Music music = musicService.getById(musicId,userId);
if(music == null){
return new ResponseMessage(-1,"要删除的音乐不存在",false);
}
int n = musicService.deleteById(ids.get(i),userId);
String path = music.getTitle();
String url = music.getUrl();
String suffix = url.substring(url.lastIndexOf("."));
path = SAVE_PATH+userId+"/"+path+suffix;
File file = new File(path);
if(file.delete()){
int m = loveMusicService.deleteLoveMusicByMusicId(musicId,userId);
sum += n;
}else {
return new ResponseMessage(-1,"删除失败",false);
}
}
if(sum == ids.size()){
return new ResponseMessage(1,"删除成功",true);
}else {
return new ResponseMessage(-1,"删除失败",false);
}
}
查询和删除sql:
<select id="getById" resultType="com.example.demo.model.Music">
select * from music where id=#{id} and user_id=#{userId}
</select>
<delete id="deleteById">
delete from music where id=#{id} and user_id=#{userId}
</delete>
<delete id="deleteLoveMusicByMusicId">
delete from lovemusic where music_id=#{musicId} and user_id=#{userId}
</delete>
7. 收藏列表
7.1 查询收藏的音乐
前端设计
查询收藏音乐的功能与前面在音乐列表查询的功能类似,页面一加载就要发送请求,获取该用户全部收藏的音乐此时的查询是查询内容为空时的查询,当用户输入名称点击查询按钮时,就要根据该用户输入的名称进行模糊查询
后端设计
在跳转到收藏列表后,后端就要查询到该用户所有的收藏音乐返回给前端,当然用户页可以通过音乐名称来查询收藏的音乐,所以在查询的时候需要判断用户输入的音乐名称是否为空,如果为空就查询当前用户所有收藏的音乐,如果不为空,就根据用户输入的音乐名称进行模糊匹配
//查询当前用户收藏的音乐
@RequestMapping("/findlovemusic")
public ResponseMessage findLoveMusic(String musicName,HttpServletRequest req){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
//获取到该用户id
int userId = user.getId();
//如果传递的参数为空,就查询所有音乐
if(musicName == null){
List<Music> musics = loveMusicService.getLoveMusicByUserId(userId);
return new ResponseMessage(1,"查询成功",musics);
}
//传递参数不为空,根据查询内容模糊匹配
List<Music> musics = loveMusicService.getLoveMusicByMusicName(musicName,userId);
return new ResponseMessage(1,"查询成功",musics);
}
查询sql:
<select id="getLoveMusicByUserId" resultType="com.example.demo.model.Music">
select * from music where id in (select music_id from lovemusic where user_id=#{userId})
</select>
<select id="getLoveMusicByMusicName" resultType="com.example.demo.model.Music">
select * from music where id in (select music_id from lovemusic where user_id=#{userId}) and title like concat('%',#{musicName},'%')
</select>
7.2 取消收藏
前端设计
用户点击某音乐的对应的收藏按钮时,携带该音乐id发送请求,后端接收到该音乐id后就会将该音乐从收藏表中删掉,待后端返回响应后,如果取消收藏成功,就刷新当前用户收藏列表页面,如果取消失败,提示错误信息
后端设计
取消收藏只是将音乐从收藏表中删除,不会从音乐表中删除,也不会删除该音乐文件
//取消当前用户收藏的某个音乐
@RequestMapping("/deletelovemusic")
public ResponseMessage deleteLoveMusic(@RequestParam String id, HttpServletRequest req){
//获取音乐id
Integer musicId = Integer.parseInt(id);
User user = (User)req.getSession().getAttribute("user");
//获取用户id
Integer userId = user.getId();
//根据用户id和音乐id,删除收藏表中对应的数据
int n = loveMusicService.deleteLoveMusic(userId,musicId);
if(n == 1){
return new ResponseMessage(1,"取消收藏成功",true);
}
return new ResponseMessage(-1,"取消失败",false);
}
删除sql:
<delete id="deleteLoveMusic">
delete from lovemusic where user_id=#{userId} and music_id=#{musicId}
</delete>
8. 注销功能
用户点击注销按钮,删除session中保存的用户信息,重定向到登陆页面
//注销
@RequestMapping("/logout")
public void logout(HttpServletRequest req,HttpServletResponse resp) throws IOException {
HttpSession session = req.getSession();
session.removeAttribute("user");
resp.sendRedirect("../login.html");
}
转载:https://blog.csdn.net/qq_58710208/article/details/127755080