文章目录
- 1. 项目描述:
- 2. 项目上线展现:
- 3. 项目具体实现:
- 1. 登录
- 2. 注册
- 3.退出系统
- 4.添加音乐
- 4.1前后端交互约定
- 4.2上传文件业务逻辑:
- 4.3创建model包中的music类
- 4.4在MusicMapper接口中,声明insertMusic抽象方法
- 4.5在mybatis包中添加操作数据的SQL语句
- 4.6 实现控制层controller中的接口方法
- 4.7 在事务层中创建MusicService接口中的insertMusic抽象方法
- 4.8 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
- 4.9使用postman进行测试
- 4.10.前端代码:
- 5. 播放音乐
- 5.1前后端交互约定
- 5.2定义后端交互接口
- 5.3 在MusicService中定义描述播放音乐的接口
- 5.4在service包下的impl子包中实现接口中的getMusic抽象方法
- 5.5ResponseEntiy类介绍
- 5.6 播放音乐业务层实现逻辑
- 5.7 使用postman进行测试
- 5.8前端代码
- 6. 删除音乐(单个删除,批量删除)
- 6.1 单个删除
- 6.1.1 前后端交互约定
- 6.1.2 删除业务实现逻辑描述
- 6.1.3 在MusicMapper中声明删除单个音乐的接口和根据id查找音乐的接口
- 6.1.4 在mybatis包中添加操作数据的SQL语句
- 6.1.5后端实现接口
- 6.1.6 在业务层中创建MusicService接口中的deleteMusic抽象方法
- 6.1.7 在业务层中service包中的impl子包中实现deleteMusic抽象方法(业务框架)
- 6.1.8 使用postman进行测试
- 6.1.9前端代码
- 6.2 实现批量删除
- 6.2.1 前后端交互约定
- 6.2.2 实现后端交互接口(是否批量删除成功)
- 6.2.3 批量删除音乐逻辑描述
- 6.2.4业务层中创建MusicService接口中的deletePartMusic抽象方法
- 6.2.5在业务层中service包中的impl子包中实现deletePartMusic抽象方法(业务框架)
- 6.2.6使用postman进行测试
- 6.2.7 前端代码
- 7. 查询音乐(支持模糊匹配)
- 7.1 前后端交互约定
- 7.2在MusicMapper中声明查询音乐的接口
- 7.3 在mybatis包中添加操作数据的SQL语句
- 7.4 后端实现接口
- 7.5 在业务层中service包中的impl子包中实现findMusic抽象方法(业务框架)
- 7.6 使用postman进行测试
- 8. 添加收藏音乐
- 8.1前后端交互约定
- 8.2 在model层下添加loveMuisic实体类
- 8.3 添加收藏音乐的具体逻辑实现:
- 8.4 在LoveMusicMapper中声明添加收藏音乐的接口
- 8.5 在mybatis包中添加操作数据的SQL语句
- 8.6 实现后端交互接口
- 8.7 在业务层中创建LoveMusicService接口中的insertMusic抽象方法
- 8.8 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
- 8.9 使用postman 进行测试
- 8.10 前端代码
- 9. 删除收藏音乐
- 9.1 前后端交互约定
- 9.2 在LoveMusicMapper中声明删除收藏音乐的接口
- 9.3 在mybatis包中添加操作数据的SQL语句
- 9.4 实现后端交互接口
- 9.5在业务层中创建LoveMusicService接口中的insertMusic抽象方法
- 9.6 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
- 9.7 使用postman 进行测试
- 9.8 前端代码
- 10.查询收藏音乐(模糊查询)
- 10.1 前后端交互约定
- 10.2在LoveMusicMapper中声明删除收藏音乐的接口
- 10.3在mybatis包中添加操作数据的SQL语句
- 10.4在业务层中创建LoveMusicService接口中的findMusic抽象方法
- 10.5 使用postman进行测试
- 11.代码完善
- 11.设置登录拦截器
- 12.使用服务器部署上线
1. 项目描述:
主要业务:注册,登录,注销,新增,查询,删除,播放歌曲。
- 在业务方面实现了基础的增删查改;
- 基于BCrypt对用户所传的密码进行加密;
- 实现了自定义登录拦截器;
- 使FileputStream,FileReader 判断上传文件是否是.mp3类型的文件
技术选型:Java,Spring,SpringMVC,SpringBoot,AJAX,MySQL,MyBatis,html,css,Js,redis
2. 项目上线展现:
3. 项目具体实现:
一. SpringBoot项目搭建:
二. 设计数据库表
-
user表
(存储用户信息)- 用户Id——
userId
- 用户名——
username
- 用户密码——
password
drop database if exists onlinemusic; create database if not exists onlinemusic character set utf8; use onlinemusic; drop table if exists user; create table user( id int primary key auto_increment, username varchar(20) not null, `password` varchar(255) not null);
- 用户Id——
-
music表
(存储音乐数据)- 哪个用户上传的这首音乐,得到这个用户的Id ——
userId
- 歌曲标题——
title
- 歌手——
singer
- 时间——
time
- 歌曲Id——
id
- url
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, userId int(11) not null);
- 哪个用户上传的这首音乐,得到这个用户的Id ——
-
lovemuisc表
(存储用户收藏的音乐信息)- 收藏音乐的
id
- 收藏音乐在music表中的
music_id
- 收藏这首歌的用户
user_id
drop table if exists lovemusic; create table lovemusic( id int primary key auto_increment, user_id int(11) not null, music_id int(11) not null);
- 收藏音乐的
三. 配置文件
# 连接数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 配置xml
mybatis.mapper-locations=classpath:mybatis/**Mapper.xml
# 配置springboot上传文件的大小,默认每个文件的配置最大为15Mb,单次请求的文件的总数不能大于100Mb
spring.servlet.multipart.max-file-size=15MB
spring.servlet.multipart.max-request-size=100MB
## 音乐上传后的路径
music.local.path=C:/work/local/music/
## 设置日志级别
logging.level.root=INFO
logging.level.com.example.onlinemusic.mapper=debug
logging.level.druid.sql.Statement=DEBUG
logging.level.com.example=DEBUG
1. 登录
-
定义model层中的实体类:
根据数据库中的user表下的字段,我们需要在实体类中 定义一个User类,该类中的属性有:userId,username,password。
@Data public class User { public int id; public String username; public String password; } //使用注解@Data 可以是代码简化,@Data中有类中的getter,setter,构造方法等功能
-
前后端交互约定:
-
定义操作数据库的抽象方法:
创建一个mapper包,在包下创建一个UserMapper接口,在接口中定义一个login(String username,String password)的抽象方法。
/** * 根据用户名查找用户 */ User findByName(String username);
首先通过username判断数据库中有没有这个用户,如果没有那么就在返回体中说明该用户不存在。
/** * 判断密码是否正确 */ User findUser(String username, String password);
-
在UserMapper.xml中实现具体的操作user数据库的操作语句:
<select id="findByName" resultType="com.example.onlinemusic.model.User"> select * from user where username = #{username} </select>
-
使用一个类约定后端服务器向前端服务器发送的响应体(ResponseBodyMessage)
定义一个tools工具包,在工具包中实现同一响应体,响应体中包括:状态(status),返回信息(message),返回具体数据(data泛型类对象)
package com.example.onlinemusic.tools;
import lombok.Data;
/**
* 封装统一响应体
*/
@Data
public class ResponseBodyMessage <T>{
private String message; //返回错误信息
private int status; //返回状态码
private T data; //返回给前端数据
public ResponseBodyMessage(String message, int status, T data) {
this.message = message;
this.status = status;
this.data = data;
}
}
-
实现前后端交互接口:
@RestController @RequestMapping("/user") public class UserController { @Resource private UserServiceImpl userService; /** * 注册 */ @PostMapping("/register") public ResponseBodyMessage<User> register(@RequestParam String username, @RequestParam String password) { //判断传入参数是否为空 if (StringUtils.isAnyBlank(username, password)) { return new ResponseBodyMessage<>("参数异常",0,null); } Boolean register = userService.register(username, password); if(!register){ return new ResponseBodyMessage<>("注册失败",-1,null); } return new ResponseBodyMessage<>("注册成功",1,null); } }
各个注解回顾:
-
@RequestParam
: @RequestParam注解用于将方法的参数与Web请求的传递的参数进行绑定。使用@RequestParam可以轻松的访问HTTP请求参数的值。 简单的说就是这个字段命名前端叫什么,我后端服务器就叫什么。
-
@RestController
:用于返回JSON,XML等数据,但不能返回HTML页面,相当于注解ResponseBody和注解controller的结合 -
@RequestMapper
:如果用在类上,则表示所有响应请求的方法都以该地址作为父路径 -
@Resource
:表示DI 注入对象 -
@PostMapper
:@PostMapping注解用于处理HTTP POST请求,并将请求映射到具体的处理方法中。@PostMapping与@GetMapping一样,也是一个组合注解,
-
-
密码加密介绍
-
使用MD5加密:
MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能的带原始的明文,那么这个过程就是不可逆的,但是虽然是不可逆的,但是这种方法也存在这个风险,因为在后来因为彩虹表的出现,这种MD5加密之后的面就没有那么有保密性了。
彩虹就是一个庞大的,针对各种可能的自核预先极端号哈希值的集合,不一定是针对MD5算法的,各种算法都有,现在的彩虹表都有100G以上的。
不安全的原因:
- 暴力攻击的速度很快
- 字典表很大
- 碰撞
更安全的做法是
加盐
或者是把密码设置长一点,让加密的字符串变长,破解的时间就会变慢,密码破解要结合它解密之后带来的经济效益。我们这里就是一个小系统,不至于人家那彩虹表给你破解密码。
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>
public class MD5Util { public static final String str = "1z2h3o4u5j6i7n"; public static String md5(String str){ return DigestUtils.md5DigestAsHex(str.getBytes()); } public static String intPutPassFromNewPass(String password){ String s = "" + str.charAt(0) + str.charAt(5) + str + str.charAt(6) + str.charAt(2) + str.charAt(3); return md5(s); } public static void main(String[] args) { String s = intPutPassFromNewPass("123456"); System.out.println(s); String s1 = intPutPassFromNewPass(s); System.out.println(s1); } }
同一个明文密码,在进行两次加密之后得到的运行结果:
568a03aca5deb1c401e2f9028d7fd150
568a03aca5deb1c401e2f9028d7fd150我们可以看到两次加密的结果是一样的,并且每次运行这个加密程序得到的都是同一种结果。
其实在我们的小系统中使用这中加密方式也是可以的,毕竟我们的系统应该不会有人破解密码码,一点利益价值都没有😁😁😁😁😁
-
使用BCrypt加密:
BCrypt也是一个加密方式,可以比较方便的实现数据的加密,我们可以简单的理解在这个类中的加密方法,有一种动态加盐的操作,我们使用的MD5加密,每次加密后的密文其实都是一样的,这样方便了MD5通过大量数据方式进行破解,我们这里的BCrypt生成的密文是60位的,MD5生成的密文是32位的,破解更难
添加依赖:
<!-- 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>
项目中没有使用到 spring security 这个框架、只是使用到了该框架下的一个类
@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class}) public class OnlinemusicApplication { public static void main(String[] args) { SpringApplication.run(OnlinemusicApplication.class, args); } }
public class BCryptTest { public static void main(String[] args) { //模拟从前端获得的密码 String password = "123456"; BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String newPassword = bCryptPasswordEncoder.encode(password); System.out.println("加密的密码为: "+newPassword); boolean same_password_result = bCryptPasswordEncoder.matches(password,newPassword); System.out.println("加密的密码和正确密码对比结果: "+same_password_result); boolean other_password_result = bCryptPasswordEncoder.matches("987654",newPassword); System.out.println("加密的密码和错误的密码对比结果: " + other_password_result); } }
加密的密码为: $2a 10 10 10Xykg/goK.CKbDJJGqSgDp.q6a/MgZBbHU/2Vc27Y81OHnBSAMcmay
加密的密码和正确密码对比结果: true
加密的密码和错误的密码对比结果: falseencode方法
:对用户密码进行加密matches方法
:第一个参数,表示的是还没有加密的密码,第二个参数表示的是从数据中查询到的加密之后的密码总结:
- 密码学的应用安全,是家里在破解所要付出的成本远超过得到的利益上的
- 使用BCrypt相比于MD5加密更好在于,破解的难度上加大了
- BCrypt的破解成本增加了,导致运行成本也大大的增加
- 总之我们这里使用MD5就已经足够了
-
MD5和BCrypt之间的区别:
- BCrypt加密:一种加盐的单向hash,不可逆的加密算法,同一种明文,每次加密后的密文都是不一样的,并且不能反向破解生成明文,破解难度很大
- MD5加密:是不加盐的单向hash,不可逆的加密算法,同一个密码经过hash的时候生成同一个hash值,在大多数的情况下,有些进过MD5加密的方法会被破解
-
-
登录具体业务实现:
@Slf4j
@Service
public class UserServiceImpl implements UserService {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
@Autowired
private UserMapper userMapper;
@Override
public User login(String username,String password, HttpServletRequest request) {
User loginUser = userMapper.findByName(username);
//用户不存在
if(loginUser == null){
return null;
}
//得到数据库中的符合username的user对象,判断user此时已经加密的密文和此时用户输入的明文密码是否相同
boolean flg = bCryptPasswordEncoder.matches(password, loginUser.getPassword());
//如果是false,就是登录失败
if(!flg){
return null;
}
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setId(loginUser.getId());
request.getSession().setAttribute(USERINFO_SESSION_KEY,user);
return loginUser;
}
@Service
public interface UserService {
/**
* 登录
*/
User login(String username,String password, HttpServletRequest request);
}
-
把用户的基本 信息添加到session中,在服务器中保存用户的登录态(这部分在后期会使用redis进行更改)
为了方便标准,在tools表下定义一个Constant类,在类中使用一个静态变量表示用户的登录信息
public class Constant { public static final String USERINFO_SESSION_KEY = "user"; }
-
使用postman测试
-
前端代码
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>注册界面</title> <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"> <link rel="stylesheet" href="css/bootstrap.css"> <link href="iconfont/style.css" type="text/css" rel="stylesheet"> <style> body { color: #fff; font-family: "微软雅黑"; font-size: 14px; } .wrap1 { position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto } /*把整个屏幕真正撑开--而且能自己实现居中*/ .main_content { background: url(images/main_bg.png) repeat; margin-left: auto; margin-right: auto; text-align: left; float: none; border-radius: 8px; } .form-group { position: relative; } .login_btn { display: block; background: #3872f6; color: #fff; font-size: 15px; width: 100%; line-height: 50px; border-radius: 3px; border: none; } .login_input { width: 100%; border: 1px solid #3872f6; border-radius: 3px; line-height: 40px; padding: 2px 5px 2px 30px; background: none; } .icon_font { position: absolute; bottom: 15px; left: 10px; font-size: 18px; color: #3872f6; } .font16 { font-size: 16px; } .mg-t20 { margin-top: 20px; } @media (min-width: 200px) { .pd-xs-20 { padding: 20px; } } @media (min-width: 768px) { .pd-sm-50 { padding: 50px; } } #grad { background: -webkit-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Safari 5.1 - 6.0 */ background: -o-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Opera 11.1 - 12.0 */ background: -moz-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Firefox 3.6 - 15 */ background: linear-gradient(#4990c1, #52a3d2, #6186a3); /* 标准的语法 */ } </style> </head> <body style="background:url(images/bg.jpg) no-repeat;"> <div class="container wrap1" style="height:450px;"> <h2 class="mg-b20 text-center">onlineMusic登录页面</h2> <div class="col-sm-8 col-md-5 center-auto pd-sm-50 pd-xs-20 main_content"> <p class="text-center font16">用户登录</p> <div class="form-group mg-t20"> <i class="icon-user icon_font"></i> <input type="text" class="login_input" id="username" placeholder="请输入用户名"/> </div> <div class="form-group mg-t20"> <i class="icon-lock icon_font"></i> <input type="password" class="login_input" id="password" placeholder="请输入密码"/> </div> <input type="submit" class="login_btn" value="登录" id="submit"> </div> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> $(function () { $("#submit").click(function () { //得到输入框中的输入内容 let username = $("#username").val(); let password = $("#password").val(); //判断空值 if (username.trim() === "" || password.trim() === "") { alert("输入内容不能为空!"); return; } $.ajax({ url:"/user/login",//指定路径 data:{"username":username,"password":password}, type:"POST", dataType:"json",//服务器返回数据为json success:function (data) { console.log(data); if(data.status===1){ alert("登录成功!"); window.location.href="list.html"; }else{ alert("登录失败,账号或密码错误,请重试!"); $("#message").text("账号或密码错误,请重试!"); $("#user").val(""); $("#password").val(""); } } }); }) }) </script> </body> </html>
2. 注册
主要实现思想:用户前端传来username和password,然后在业务层判断数据库中是否存在这个username,如果存在,那么直接返回false,否则就把用户添加到数据库中,返回true
❤️❤️在UserMapper接口中定义抽象方法register(String username,String password)****
/**
* 注册用户信息
*/
Boolean register(String username,String password);
❤️❤️在resource 资源包下中的UserMapper.xml中添加SQL语句(在数据库中添加用户)
<insert id="register" >
insert into user values(null,#{username},#{password});
</insert>
😁😁实现前后端交互接口中的register()方法,如果传来的两个参数为空,或者字符串的长度为0,那么就返回参数异常,如果在数据库查询这个注册用户已经存在了,那么就返回-1,如果注册失败就返回-2
@PostMapping("/register")
public ResponseBodyMessage<User> register(@RequestParam String username, @RequestParam String password) {
//判断传入参数是否为空
if (StringUtils.isAnyBlank(username, password)) {
return new ResponseBodyMessage<>("参数异常",0,null);
}
int register = userService.register(username, password);
if(register == -1){
return new ResponseBodyMessage<>("该用户已存在",-1,null);
}else if(register == -2){
return new ResponseBodyMessage<>("注册失败",-2,null);
}
return new ResponseBodyMessage<>("注册成功",1,null);
}
😁😁UserService中的register()抽象方法
/**
* 注册
*/
int register(String username,String password);
😁😁UserService包下的impl包下的UserServiceImpl类实现register方法
@Override
public int register(String username, String password) {
//判断该用户在数据库中是否存在
User user = userMapper.findByName(username);
if(user != null){
return -1;
}
//进行加密
password = bCryptPasswordEncoder.encode(password);
Boolean register = userMapper.register(username, password);
if(!register){
return -2;
}
return 1;
}
使用postman进行测试;
前端代码:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>注册界面</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<link rel="stylesheet" href="css/bootstrap.css">
<link href="iconfont/style.css" type="text/css" rel="stylesheet">
<style>
body {
color: #fff;
font-family: "微软雅黑";
font-size: 14px;
}
.wrap1 {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto
}
/*把整个屏幕真正撑开--而且能自己实现居中*/
.main_content {
background: url(images/main_bg.png) repeat;
margin-left: auto;
margin-right: auto;
text-align: left;
float: none;
border-radius: 8px;
}
.form-group {
position: relative;
}
.login_btn {
display: block;
background: #3872f6;
color: #fff;
font-size: 15px;
width: 100%;
line-height: 50px;
border-radius: 3px;
border: none;
}
.login_input {
width: 100%;
border: 1px solid #3872f6;
border-radius: 3px;
line-height: 40px;
padding: 2px 5px 2px 30px;
background: none;
}
.icon_font {
position: absolute;
bottom: 15px;
left: 10px;
font-size: 18px;
color: #3872f6;
}
.font16 {
font-size: 16px;
}
.mg-t20 {
margin-top: 20px;
}
@media (min-width: 200px) {
.pd-xs-20 {
padding: 20px;
}
}
@media (min-width: 768px) {
.pd-sm-50 {
padding: 50px;
}
}
#grad {
background: -webkit-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Safari 5.1 - 6.0 */
background: -o-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Opera 11.1 - 12.0 */
background: -moz-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Firefox 3.6 - 15 */
background: linear-gradient(#4990c1, #52a3d2, #6186a3); /* 标准的语法 */
}
</style>
</head>
<body style="background:url(images/bg.jpg) no-repeat;">
<div class="container wrap1" style="height:450px;">
<h2 class="mg-b20 text-center">onlineMusic注册页面</h2>
<div class="col-sm-8 col-md-5 center-auto pd-sm-50 pd-xs-20 main_content">
<p class="text-center font16">用户注册</p>
<div class="form-group mg-t20">
<i class="icon-user icon_font"></i>
<input type="text" class="login_input" id="username" placeholder="请输入用户名"/>
</div>
<div class="form-group mg-t20">
<i class="icon-lock icon_font"></i>
<input type="password" class="login_input" id="password" placeholder="请输入密码"/>
</div>
<div class="form-group mg-t20">
<i class="icon-lock icon_font"></i>
<input type="password" class="login_input" id="confirmPassword" placeholder="请输入确认密码"/>
</div>
<input type="submit" class="login_btn" value="注册" id="submit">
</div>
</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
$(function () {
$("#submit").click(function () {
//得到输入框中的输入内容
let username = $("#username").val();
let password = $("#password").val();
let confirmPassword = $("#confirmPassword").val();
//判断空值
if (username.trim() === "" || password.trim() === "" || confirmPassword.trim() === "") {
alert("输入内容不能为空!");
return;
}
if (password !== confirmPassword) {
alert("密码和确认密码要相同!")
return;
}
$.ajax({
url: '/user/register',
data:{"username":username,"password":password},
type:"POST",
dataType:"json",//服务器返回数据为json
success:function (data) {
console.log(data);
if(data.status===1){
alert("注册成功!");
window.location.href="login.html";
}else if(data.status === -1){
alert("该用户已存在");
}else{
alert("注册失败");
}
}
});
})
})
</script>
</body>
</html>
3.退出系统
因为退出系统的业务条件比较简单,我们此时在controller(程序调用接口层)进行实现
😁😁 其实退出系统的业务非常简单:就是把服务器中记录用户信息的sessionId 对应的信息给删除就行。
@PostMapping("/logout")
public ResponseBodyMessage<Boolean> logout(HttpServletRequest request){
//得到此时用户的登录态,设置登录态为空
request.getSession().removeAttribute(USERINFO_SESSION_KEY);
return new ResponseBodyMessage<>("退出登录",0,true);
}
使用postman 进行测试:
前端代码:
//退出登录
$("#logout").click(function (){
$.ajax({
url: '/user/logout',
type: 'post',
success: function (result) {
if (result) {
alert("退出成功,返回登录页面");
window.location.assign("login.html");
} else {
alert("退出失败");
}
}
})
})
4.添加音乐
4.1前后端交互约定
4.2上传文件业务逻辑:
首先用户在前端页面上传一个文件,然后经过前端传递给后端。
- 判断当前用户是否是登录状态
- 判断传来的文件是否在数据库中已经存在了,如果存在就提醒用户该文件已存在。
- 然后判断文件的类型,此处的判断文件类型不能只判断文件是否已.mp3结尾,我们要知道上传.mp3文件的标准是什么,其实就是在mp3文件中字节码文件中用一个标志位使用
TAG
表示的,我们可以通过它判断文件的类型 - 然后就是把文件上传到服务器中
- 把文件添加到数据库中
4.3创建model包中的music类
因为此时要上传音乐,所以我们在model包中创建一个music实体类
package com.example.onlinemusic.model;
import lombok.Data;
@Data
public class Music {
public int id; //音乐id
public String title; //音乐标题
public String singer; //歌手
public String time; //时间
public String url; //url
public int userId; //上传音乐用户的id
}
4.4在MusicMapper接口中,声明insertMusic抽象方法
❤️❤️在mapper包下的MusicMapper接口中,声明一个用于把歌曲信息写到数据库中的抽象方法
/**
* 添加音乐
*/
int insertMusic(String title,String singer,String time,String url,int userId);
4.5在mybatis包中添加操作数据的SQL语句
❤️❤️在mybatis包下添加一个用于在数据库中存储歌曲信息的SQL语句,把歌曲名,歌手,时间,url(播放歌曲的时候用得到),和上传这个用户的Id
<insert id="insertMusic">
insert into music(title,singer,time,url,userId) values(#{title},#{singer},#{time},#{url},#{userId})
</insert>
4.6 实现控制层controller中的接口方法
@PostMapping("/upload")
public ResponseBodyMessage<Boolean> insertMusic(@RequestParam String singer,
@RequestParam("filename") MultipartFile multipartFile,
HttpServletRequest request) throws IOException {
if(StringUtils.isAnyBlank(singer)){
return new ResponseBodyMessage<>("参数异常",-1,false);
}
int ret = musicService.insertMusic(singer, multipartFile, request);
if(ret == -1){
return new ResponseBodyMessage<>("用户还未登录,请先登录",-1,false);
}else if(ret == 0){
return new ResponseBodyMessage<>("歌曲已经上传过了,无需再次上传",0,false);
}else if(ret == -2){
return new ResponseBodyMessage<>("上传文件失败",-2,false);
}else if(ret == -3){
return new ResponseBodyMessage<>("上传文件的格式不对",-3,false);
}
return new ResponseBodyMessage<>("文件存储成功",1,true);
}
4.7 在事务层中创建MusicService接口中的insertMusic抽象方法
接口层调用业务层,创建一个MusicService接口,在接口中声明一个添加音乐的抽象方法。在Service包中的Impl子包中添加一个MusicServiceImpl用于实现接口中的方法。
int insertMusic(String singer, MultipartFile multipartFile, HttpServletRequest request)
4.8 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
因为我们要把文件上传到服务器和数据库中,因为我们现在是本地开发的,那么此时就在本地设置一个存放音乐数据的地方,其实我们在配置文件的时候,已经执行的。把文件添加到
C:/work/local/music/ 使用@Value(“${music.local.path}”),获得到配置文件中的值。
这里我们还要介绍一个类:MultipartFile,是Spring框架中处理文件上传的主要类。
主要的方法有:
@Autowired
private MusicMapper musicMapper;
//读取配置文件中的信息 --- 歌曲所在的盘福路径
@Value("${music.local.path}")
private String SAVE_PATH;
//得到客户端发来的歌手,歌曲,和判断此时用户是否已经登录成功
public int insertMusic(String singer, @RequestParam("file")MultipartFile file, HttpServletRequest request) {
//检查此时用户是否登录
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(USERINFO_SESSION_KEY) == null) {
return -1;
}
//判断传出的文件是否是mp3文件 使用得到字节数组中的最后一段字节码,判断二进制字符串中是否存在TAG
boolean isNotMp3Type = false;
try {
InputStream is = file.getInputStream();
InputStreamReader isReader = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isReader);
//循环逐行读取
String line;
while ((line = br.readLine()) != null) {
if(line.contains("TAG")){
isNotMp3Type = true;
}
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
if(!isNotMp3Type){
return -3;
}
//把歌曲文件上传到服务器
//可以得到上传文件的名称和类型
String fileNameAndType = file.getOriginalFilename();
assert fileNameAndType != null;
int index = fileNameAndType.lastIndexOf(".");
String title = fileNameAndType.substring(0, index);
//判断数据库中是否存在和要即将添加的音乐重名并且歌手名相同,如果相同就是重复的歌曲,题型用户
Music music = musicMapper.findMusicNyTitleAndSinger(title, singer);
//歌曲已经上传过了
if(music != null){
return 0;
}
//得到存放音乐文件的路径
//盘福路径 + 歌曲名称
String path = SAVE_PATH + fileNameAndType;
File file1 = new File(path);
//如果该目录不存在,那么就重新创建一个
if (!file1.exists()) {
file1.mkdir();
}
try {
//向指定目录中上传音乐
file.transferTo(file1);
} catch (IOException e) {
e.printStackTrace();
//服务器存储失败
return -2;
}
//把文件上传的数据库中
//文件标题
//得到文件名中 “.”的位置,截取到"."这个位置,就是title
//得到userId
User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);
int userId = user.getId();
//得到url 此处的url 用于播放
String url = "/music/get?path=" + title;
//得到时间
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String time = simpleDateFormat.format(new Date());
//如果此时的数据库存放文件失败,那么此时的服务器中的文件也应该消失没有
try{
int ret = musicMapper.insertMusic(title, singer, time, url, userId);
//数据库存储失败
if(ret != 1){
return -2;
}
return 1;
}catch (BindingException e){
//如果在把文件信息添加到数据库的时候发生了异常,那么此时就把服务器中的文件也要删了
//如果不删,那么此时服务器和数据库中的文件数据就不一致了
file1.delete(); //删除服务器中的文件
return -2;
}
}
1.判断用户是否登录
//检查此时用户是否登录
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(USERINFO_SESSION_KEY) == null) {
return -1;
}
2.判断删除的文件是否满足mp3文件格式:
我们不能是否文件的后缀来判断,某个文件是否是.mp3文件,因为谁知道那个老六会把其他类型的文件的后缀名改为.mp3文件。
其实每个文件都有自己的组成方式,在每个文件的尾部,长度为128字节,有一个.mp3公有的特点,就是有TAG标识
读取文件中的信息,把这些信息转化成为utf-8类型,然后进行逐行读取,判断读取的每一行中是否有"TAG"字段
InputStream 只是一个抽象类
,要使用还需要具体的实现类
。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使
用 FileInputStream
转化字符集类型
InputStreamReader(InputStream in, Charset cs)
创建一个使用给定字符集的InputStreamReader
BufferedReader 从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取
//判断传出的文件是否是mp3文件 使用得到字节数组中的最后一段字节码,判断二进制字符串中是否存在TAG
boolean isMp3Type = false;
try {
InputStream is = file.getInputStream();
InputStreamReader isReader = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isReader);
//循环逐行读取
String line;
while ((line = br.readLine()) != null) {
if(line.contains("TAG")){
isMp3Type = true;
}
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
if(!isMp3Type){
return -3;
}
3.判断数据库中是否已经存在这个文件
使用getOriginalFilename方法得到这个文件的名字和类型,然后在得到"."的下标,然后再使用substring()方法得到0~.之间的字符串,这就是文件的具体名字,即title,然后根据这个title和singer判断数据库中是否存在这个singer所唱的title ,因为同一首歌会有许多人唱,所以使用singer和title在数据库中匹配。
String fileNameAndType = file.getOriginalFilename();
assert fileNameAndType != null;
int index = fileNameAndType.lastIndexOf(".");
String title = fileNameAndType.substring(0, index);
//判断数据库中是否存在和要即将添加的音乐重名并且歌手名相同,如果相同就是重复的歌曲,题型用户
Music music = musicMapper.findMusicNyTitleAndSinger(title, singer);
//歌曲已经上传过了
if(music != null){
return 0;
}
4.如果此时在数据库中未找到这个音乐,那么此时就把文件上传到服务器中。
此时得到服务器中要存放文件的盘符路径 和 这个文件的文件名和后缀,拼接,判断这个文件路径是否已经存在,如果不存在,使用mkdir创建一个。然后使用transferTo()方法,把文件上传到指定目录。
//得到存放音乐文件的路径
//盘福路径 + 歌曲名称
String path = SAVE_PATH + fileNameAndType;
File file1 = new File(path);
//如果该目录不存在,那么就重新创建一个
if (!file1.exists()) {
file1.mkdir();
}
try {
//向指定目录中上传音乐
file.transferTo(file1);
} catch (IOException e) {
e.printStackTrace();
//服务器存储失败
return -2;
}
5.把文件相关内容写到数据库中
此时我们要把相关这个文件的 title(文件名),userId(上传文件的userId),time(上传文件的时间),singer(歌手),url传到数据库。
//得到userId
User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);
//根据已经登录的session信息得到此时用户的Id
int userId = user.getId();
//得到url 此处的url 用于播放
String url = "/music/get?path=" + title;
//得到时间
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String time = simpleDateFormat.format(new Date());
//如果此时的数据库存放文件失败,那么此时的服务器中的文件也应该消失没有
try{
int ret = musicMapper.insertMusic(title, singer, time, url, userId);
//数据库存储失败
if(ret != 1){
return -2;
}
return 1;
}catch (BindingException e){
//如果在把文件信息添加到数据库的时候发生了异常,那么此时就把服务器中的文件也要删了
//如果不删,那么此时服务器和数据库中的文件数据就不一致了
file1.delete(); //删除服务器中的文件
return -2;
}
4.9使用postman进行测试
同一个用户上传同一首歌曲是不能上传成功的
上传后缀名不是mp3的文件:
上传后缀名是.mp3的文件,但是该文件的本质不是一个mp3格式
4.10.前端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传音乐</title>
</head>
<body>
<form action="/music/upload" method="post" enctype="multipart/form-data">
上传文件:<input type="file" name="filename">
<label>
歌手名:<input type="text" name="singer" placeholder="请输入歌手名">
</label>
<input type="submit" value="上传">
</form>
</body>
</html>
5. 播放音乐
5.1前后端交互约定
5.2定义后端交互接口
@GetMapping("/get")
public ResponseEntity<byte[]> getMusic(@RequestParam String path){
//如果客户端传来的path为空,或者字符串长度为0,那么就是一个异常参数,那么就是一个有问题的请求
if(StringUtils.isAnyBlank(path)){
return ResponseEntity.badRequest().build();
}
//返回字节类型的文件数据
return musicService.getMusic(path);
}
5.3 在MusicService中定义描述播放音乐的接口
ResponseEntity<byte[]> getMusic(String path);
5.4在service包下的impl子包中实现接口中的getMusic抽象方法
我们使用了Files.reasAllBytes(String path):读取文件中的所有字节,读入内存,参数path是文件的路径(绝对路径)
@Override
public ResponseEntity<byte[]> getMusic(String path) {
File file = new File(SAVE_PATH + path);
byte[] a = null;
try {
//转化成为字节码数据
a = Files.readAllBytes(file.toPath());
//如果a为null
if(a == null){
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(a);
} catch (IOException e) {
e.printStackTrace();
}
return ResponseEntity.badRequest().build();
}
5.5ResponseEntiy类介绍
ResponseEntity对象是Spring对请求响应的封装。它继承了HttpEntity对象,包含了Http的响应码(httpstatus)、响应头(header)、响应体(body)三个部分。
ResponseEntity类继承自HttpEntity类,被用于Controller层方法 。ResponseEntity.ok 方法有2个方法,分别是有参数和没有参数。
//这个方法若被调用的话,返回OK状态
public static ResponseEntity.BodyBuilder ok(){
return status(HttpStatus.OK);
}
//这个方法若被调用的话,返回body内容和OK状态
public static <T> ResponseEntity<T> ok(T body) {
ResponseEntity.BodyBuilder builder = ok();
//ResponseEntity可以通过这个builder返回任意类型的body内容
return builder.body(body);
}
与API中的描述一致,无参 ok方法 返回OK状态,有参ok方法返回body内容和OK状态
body类型 是 泛型T,也就是我们不确定body是什么类型,可以向ok方法传递任意类型的值
有参ok方法其实有调用无参ok方法
5.6 播放音乐业务层实现逻辑
把传来的文件转化成为字节码文件,然后把字节码文件返回给前端
//转化成为字节码数据
a = Files.readAllBytes(file.toPath());
//如果a为null
if(a == null){
return ResponseEntity.badRequest().build();
}
5.7 使用postman进行测试
5.8前端代码
<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(result) {
console.log(result);
//得到音乐的名字
let musicName = result.substring(result.lastIndexOf("=") + 1);
console.log(musicName);
//retult 表示的是 url musicName 表示的是音乐名称 0 表示的是音乐从什么时候开始播放 false表示的是是否启动自动播放
SewisePlayer.toPlay(result, musicName, 0, true);
}
6. 删除音乐(单个删除,批量删除)
6.1 单个删除
6.1.1 前后端交互约定
6.1.2 删除业务实现逻辑描述
根据前端传来的音乐id 知道了是哪一首歌曲,如果根据这个id 在数据库中没有找到音乐文件,那么就提醒用户删除文件不存在,如果找到了该文件,那么就把数据库和服务器中的该音乐文件都给删了
6.1.3 在MusicMapper中声明删除单个音乐的接口和根据id查找音乐的接口
/**
* 根据id 查找音乐
*/
Music findMusicById(int id);
/**
* 删除单个音乐
*/
int deleteMusicById(int id);
6.1.4 在mybatis包中添加操作数据的SQL语句
添加根据id删除音乐文件和根据id查询音乐的SQL
<select id="findMusicById" resultType="com.example.onlinemusic.model.Music">
select * from music where id = #{id}
</select>
<delete id="deleteMusicById" parameterType="java.lang.Integer">
delete from music where id = #{id}
</delete>
6.1.5后端实现接口
@PostMapping("/delete")
public ResponseBodyMessage<Boolean> deleteMusic(int id){
if(id < 0){
return new ResponseBodyMessage<>("参数错误",-1,false);
}
int i = musicService.deleteMusicById(id);
if(i == -1){
return new ResponseBodyMessage<>("该音乐不存在",-1,false);
}else if(i == -2){
return new ResponseBodyMessage<>("删除失败",-2,false);
}
return new ResponseBodyMessage<>("删除成功",1,true);
}
6.1.6 在业务层中创建MusicService接口中的deleteMusic抽象方法
int deleteMusicById(int musicId);
6.1.7 在业务层中service包中的impl子包中实现deleteMusic抽象方法(业务框架)
-
使用传来的音乐id在数据库中找到该文件,如果该文件为空,那么就没有id指向的文件,那么直接返回-1
-
然后根据这个id,删除数据库music表中id对应的文件。
-
但是我们在删除服务器中文件的时候就要注意了,我们在数据库中可能不同的用户上传了同一首音乐,同时服务器中的该音乐文件只有一个。那么此时我们需要使用该文件的title和singer在数据库中查看是否还存在着同名的歌曲,如果存在,那么就不删除服务器中的文件,如果不存在,那么就删除。
//根据music 中的title和singer判断数据库中是否还有同名同名的文件,如果有就不删除服务器中的文件 //如果这首歌在数据库中不存在了,那么就删除服务器中的文件 Music musicByTitleAndSinger = musicMapper.findMusicByTitleAndSinger(music.title, music.singer); if(musicByTitleAndSinger != null){ return 1; }
@Override
public int deleteMusicById(int id) {
Music music = musicMapper.findMusicById(id);
if(music == null){
return -1;
}
//删除数据库中的音乐
int delete = musicMapper.deleteMusicById(id);
if(delete != 1){
return -2;
}
//删除服务器中的音乐
//得到文件名
int index = music.getUrl().lastIndexOf("=");
String filename = music.getUrl().substring(index + 1);
String path = SAVE_PATH + filename + ".mp3";
File file = new File(path);
System.out.println(file.toPath());
//服务器删除失败
if(!file.delete()){
log.info("服务器删除失败");
return -2;
}
return 1;
}
6.1.8 使用postman进行测试
6.1.9前端代码
//删除音乐
function deleteSong(id) {
$.ajax({
url: '/music/delete',
type: 'post',
data: {"id": id},
dataType: 'json',
success: function (result) {
if (result) {
alert("删除成功,重新加载页面");
window.location.assign("list.html");
} else {
alert("删除失败");
}
}
})
}
6.2 实现批量删除
6.2.1 前后端交互约定
6.2.2 实现后端交互接口(是否批量删除成功)
@PostMapping("/deletepart")
public ResponseBodyMessage<Boolean> deletePartMusic(@RequestParam("id[]") List<Integer> id){
//判断id长度是否为空
if(id.size() == 0){
return new ResponseBodyMessage<>("请选择歌曲",-1,false);
}
if(!musicService.deletePart(id)){
return new ResponseBodyMessage<>("批量删除失败",-1,false);
}
return new ResponseBodyMessage<>("批量删除成功",1,true);
}
6.2.3 批量删除音乐逻辑描述
前端传来一个List,在这个List中每个元素都代表的是对应音乐文件的id,删除List中所有Id对应的音乐文件。但是还是一样的删除服务器中的文件的时候,要注意!!!
6.2.4业务层中创建MusicService接口中的deletePartMusic抽象方法
Boolean deletePart(List<Integer> id);
6.2.5在业务层中service包中的impl子包中实现deletePartMusic抽象方法(业务框架)
在数据库和服务器都把这个音乐给删了,我们就记位一次。
@Override
public Boolean deletePart(List<Integer> id) {
int sum = 0;
for(int i = 0;i < id.size();i++) {
int musicIndex = id.get(i);
Music music = musicMapper.findMusicById(musicIndex);
if (music == null) {
return false;
}
loveMusicMapper.deleteMusicById(music.getId());
//删除数据库中的文件信息
int ret = musicMapper.deleteMusicById(musicIndex);
//删除服务器中的文件
int index = music.getUrl().lastIndexOf("=");
String filename = music.getUrl().substring(index + 1);
String path = SAVE_PATH + filename + ".mp3";
File file = new File(path);
System.out.println(file.toPath());
if (file.delete()) {
sum += ret;
}else {
return false;
}
}
return sum == id.size();
}
6.2.6使用postman进行测试
6.2.7 前端代码
//当页面加载完后 批量删除
$.when(load).done(function () {
$("#delete").click(function () {
let id = new Array();
let i = 0;
//遍历被选中
$("input:checkbox").each(function () {
if ($(this).is(":checked")) {
id[i] = $(this).attr("id");
i++;
}
})
$.ajax({
url: '/music/deletepart',
type: 'post',
data: {'id': id},
dataType: 'json',
success: function (result) {
if (result) {
alert("批量删除成功");
window.location.assign("list.html");
} else {
alert("批量删除失败");
}
}
})
})
7. 查询音乐(支持模糊匹配)
7.1 前后端交互约定
7.2在MusicMapper中声明查询音乐的接口
/**
* 查询所有的音乐
*/
List<Music> findAllMusic();
/**
* 模糊匹配歌曲名
*/
List<Music> findMusicByFuzzyAndTitle(String title);
7.3 在mybatis包中添加操作数据的SQL语句
<select id="findAllMusic" resultType="com.example.onlinemusic.model.Music">
select * from music;
</select>
<select id="findMusicByFuzzyAndTitle" resultType="com.example.onlinemusic.model.Music">
select * from music where title like concat('%',#{title},'%')
</select>
7.4 后端实现接口
@GetMapping("/findmusic")
public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String title){
List<Music> musicList = musicService.findMusic(title);
if(musicList == null){
return new ResponseBodyMessage<>("查询列表为空",-1,null);
}
return new ResponseBodyMessage<>("查询成功",1,musicList);
}
7.5 在业务层中service包中的impl子包中实现findMusic抽象方法(业务框架)
@Override
public List<Music> findMusic(String title) {
if(title == null){
return musicMapper.findAllMusic();
}
return musicMapper.findMusicByFuzzyAndTitle(title);
}
7.6 使用postman进行测试
前端代码:
function load(title) {
$.ajax({
//从服务器上得到数据
url: '/music/findmusic',
type: 'get',
dataType: 'json',
data: {"title": title},
success: function (result) {
if (result == null) {
alert("没有查询到这首歌");
return;
}
console.log(result);
//在这里result是一个数组,在数组中的每个元素中包含每个歌曲的 id singer url
let data = result.data;
let s = '';
for (let i = 0; i < data.length; i++) {
let 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" οnclick="playerSong(\''+musicUrl+'\')" >播放歌曲</button>' +
'</td>';
s+='<td > <button class="btn btn-primary" οnclick="deleteSong('+ data[i].id + ')" >删除</button>' +
'<button class="btn btn-primary" οnclick="loveSong('+ data[i].id + ')" > 喜欢</button>'+
'</td>';
s += '</tr>';
}
$("#list").html(s);//把拼接好的页面添加到info的id下
}
})
}
8. 添加收藏音乐
8.1前后端交互约定
8.2 在model层下添加loveMuisic实体类
在实体类中包括 id 收藏音乐的Id,music_id 收藏音乐对应的在music表下到的Id,还有就是user_id 表示的是那个用户收藏的在user表中的id
package com.example.onlinemusic.model;
import lombok.Data;
@Data
public class LoveMusic {
public int id;
public int music_id;
public int user_id;
}
8.3 添加收藏音乐的具体逻辑实现:
根据传来的musicId和从此时的登录态中得到的userId,在数据库中的lovermusic表中查看这个音乐是否存在,如果存在就提示用户此时无需添加,已经收藏。如果lovmusic表中不存在这个文件,那么就把这个音乐添加到lovmusic中,其实就是把这个muiscId添加到了music表中。
8.4 在LoveMusicMapper中声明添加收藏音乐的接口
Music findLoveMusicByUserIdAndMusicId(int userId,int musicId);
int insertMusic(int userId, int musicId);
8.5 在mybatis包中添加操作数据的SQL语句
<select id="findLoveMusicByUserIdAndMusicId" resultType="com.example.onlinemusic.model.Music">
select * from lovemusic where music_id = #{musicId} and user_id = #{userId}
</select>
<insert id="insertMusic">
insert into lovemusic values(null,#{userId},#{musicId})
</insert>
8.6 实现后端交互接口
@Autowired
private LoveMusicServiceImpl loveMusicService;
@PostMapping("/insert")
public ResponseBodyMessage<Boolean> insertMusic(int musicId, HttpServletRequest request){
if(musicId < 0){
return new ResponseBodyMessage<>("参数错误",-1,false);
}
int insert = loveMusicService.insertMusic(musicId, request);
if(insert == -1){
return new ResponseBodyMessage<>("该歌曲已收藏",-1,false);
}else if(insert == -2){
return new ResponseBodyMessage<>("收藏失败",-2,false);
}
return new ResponseBodyMessage<>("添加成功",1,true);
}
8.7 在业务层中创建LoveMusicService接口中的insertMusic抽象方法
int insertMusic(int musicId, HttpServletRequest request);
8.8 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
@Override
public int insertMusic(int musicId, HttpServletRequest request) {
//得到当前的登录态
User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);
int userId = user.getId();
//查询音乐是否已存在
Music music= loveMusicMapper.findLoveMusicByUserIdAndMusicId(userId, musicId);
if(music != null){
return -1;
}
int ret = loveMusicMapper.insertMusic(userId, musicId);
if(ret != 1){
return -2;
}
return 1;
}
8.9 使用postman 进行测试
8.10 前端代码
function loveSong(musicId) {
console.log(musicId);
$.ajax({
url: '/lovemusic/insert',
type: 'post',
data: {"musicId": musicId},
dataType: 'json',
success: function (result) {
if (result.status === -1) {
alert("该歌曲已收藏!!!");
} else if (result.status === -2) {
alert("收藏失败!!!")
} else if(result.status === 0 ){
alert("参数错误!!!")
}else{
alert("收藏成功");
window.location.assign("list.html");
}
}
})
}
9. 删除收藏音乐
9.1 前后端交互约定
9.2 在LoveMusicMapper中声明删除收藏音乐的接口
int deleteMusic(int userId,int musicId);
9.3 在mybatis包中添加操作数据的SQL语句
<delete id="deleteMusic">
delete from lovemusic where user_id = #{userId} and music_id = #{musicId}
</delete>
9.4 实现后端交互接口
@PostMapping("/deletemusic")
public ResponseBodyMessage<Boolean> deleteMusic(int musicId,HttpServletRequest request){
if(musicId < 0){
return new ResponseBodyMessage<>("参数错误",-1,false);
}
int i = loveMusicService.deleteMusic(musicId,request);
if(i == -1){
return new ResponseBodyMessage<>("该音乐不存在",-1,false);
}else if(i == -2){
return new ResponseBodyMessage<>("删除失败",-2,false);
}
return new ResponseBodyMessage<>("删除成功",1,true);
}
9.5在业务层中创建LoveMusicService接口中的insertMusic抽象方法
int deleteMusic(int music,HttpServletRequest request);
9.6 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
@Override
public int deleteMusic(int musicId, HttpServletRequest request) {
User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);
Music music = loveMusicMapper.findLoveMusicByUserIdAndMusicId(user.getId(),musicId);
if(music == null){
return -1;
}
//删除数据库中的音乐
int delete = loveMusicMapper.deleteMusic(user.getId(),musicId);
if(delete != 1){
return -2;
}
return 1;
}
9.7 使用postman 进行测试
9.8 前端代码
删除收藏页面中的音乐逻辑和主页中的逻辑一致,这里不过多叙述
10.查询收藏音乐(模糊查询)
10.1 前后端交互约定
@GetMapping("/findmusic")
public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String title,HttpServletRequest request){
User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);
List<Music> musicList = loveMusicService.findMusic(title,user.getId());
if(musicList == null){
return new ResponseBodyMessage<>("查询列表为空",-1,null);
}
return new ResponseBodyMessage<>("查询成功",1,musicList);
}
10.2在LoveMusicMapper中声明删除收藏音乐的接口
//查看user喜欢的所有音乐
List<Music> findAllMusic(int userId);
//查看user输入的title 也就是歌曲名相关的有关音乐
List<Music> findMusicByFuzzyAndTitle(String title,int userId);
10.3在mybatis包中添加操作数据的SQL语句
<resultMap id="BaseMap1" type="com.example.onlinemusic.model.Music">
<id column="id" property="id" />
<id column="title" property="title" />
<id column="singer" property="singer" />
<id column="time" property="time" />
<id column="url" property="url" />
<id column="userid" property="userId" />
</resultMap>
<select id="findAllMusic" resultMap="BaseMap1">
select m.* from lovemusic lm,music m where m.id = lm.music_id and lm.user_id=#{userId}
</select>
<resultMap id="BaseMap2" type="com.example.onlinemusic.model.Music">
<id column="id" property="id" />
<id column="title" property="title" />
<id column="singer" property="singer" />
<id column="time" property="time" />
<id column="url" property="url" />
<id column="userid" property="userId" />
</resultMap>
<select id="findMusicByFuzzyAndTitle" resultMap="BaseMap2">
select m.* from lovemusic lm,
music m where m.id = lm.music_id and lm.user_id=#{userId} and title like concat('%',#{title},'%')
</select>
10.4在业务层中创建LoveMusicService接口中的findMusic抽象方法
List<Music> findMusic(String title,int userId);
10.5 在业务层中service包中的impl子包中实现findMusic抽象方法(业务框架)
@Override
public List<Music> findMusic(String title,int userId) {
if(title == null){
return loveMusicMapper.findAllMusic(userId);
}
return loveMusicMapper.findMusicByFuzzyAndTitle(title,userId);
}
10.5 使用postman进行测试
11.代码完善
其实我们在删除的时候应当注意,我们的music表中的数据应该和lovemusic表中的数据保持一致,也就是如果我现在music页面和lovemusic页面有有同一首歌,如果把music页面中的这一首歌给删了,那么我们的这个lovmusic表中的这个音乐的数据,那么也就没了。但是反过来是不一样的,把lovemusic表中的数据,无论我们怎么删。都是和music表是无关的。
只能是music ------> lovemusic
使用musicId删除lovemusic表中的数据
loveMusicMapper.deleteMusicById(music.getId());
修改后的deleteMusicById()
@Override
public int deleteMusicById(int id) {
Music music = musicMapper.findMusicById(id);
if(music == null){
return -1;
}
//删除数据库中的音乐
int delete = musicMapper.deleteMusicById(id);
if(delete != 1){
return -2;
}
//删除和这个音乐相关的在lovemusic中的歌曲
loveMusicMapper.deleteMusicById(music.getId());
//删除服务器中的音乐
//得到文件名
int index = music.getUrl().lastIndexOf("=");
String filename = music.getUrl().substring(index + 1);
String path = SAVE_PATH + filename + ".mp3";
File file = new File(path);
System.out.println(file.toPath());
if (!file.delete()) {
return -2;
}
return 1;
}
修改后的deletePart()
@Override
public Boolean deletePart(List<Integer> id) {
int sum = 0;
for(int i = 0;i < id.size();i++) {
int musicIndex = id.get(i);
Music music = musicMapper.findMusicById(musicIndex);
if (music == null) {
return false;
}
//删除数据库中的文件信息
int ret = musicMapper.deleteMusicById(musicIndex);
//删除服务器中的文件
int index = music.getUrl().lastIndexOf("=");
String filename = music.getUrl().substring(index + 1);
String path = SAVE_PATH + filename + ".mp3";
File file = new File(path);
System.out.println(file.toPath());
if (file.delete()) {
//删除和这个音乐相关的在lovemusic中的歌曲
loveMusicMapper.deleteMusicById(music.getId());
sum += ret;
}else {
return false;
}
}
return sum == id.size();
}
11.设置登录拦截器
首先在这里声明一点,我们此时虽然已经完成了,项目的大部分逻辑,但是如果我现在直接使用访问用户收藏音乐界面是可以访问到的。因为我们还没有设置拦截器。
创建一个用于配置拦截器的包—webConfig包,在这个包中在声明一个用户的登录拦截器。
@Configuration
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//通过session中的信息我们就知道此时的这个用户是否是登录状态
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute(Constant.USERINFO_SESSION_KEY) != null){
System.out.println("登录成功");
return true;
}
return false;
}
}
配置登录拦截器,首先拦截所有的页面,然后在逐一的把某些页面和路由解放出来,如登录,注册页面是不需要验证登录状态的,还有对应的前端页面。
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LoginInterceptor loginInterceptor = new LoginInterceptor();
registry.addInterceptor(loginInterceptor).
addPathPatterns("/**").
excludePathPatterns("/user/login").
excludePathPatterns("/user/register").
excludePathPatterns("/css/**.css").
excludePathPatterns("/js/**.js").
excludePathPatterns("/login.html").
excludePathPatterns("/register.html").
excludePathPatterns("/images/**").
excludePathPatterns("/player/**").
excludePathPatterns("/iconfont/**").
excludePathPatterns("/source/**");
}
12.使用服务器部署上线
我们为了让服务器上线,要修改resource中的配置文件
其中要改变的就是此处的url,因为要连接linux中的NMySQL,还有就是关于MySQL的密码
- 记住在服务器上线的时候,一定要在防火墙中打开MySQL的对应端口3306!!!
music:
local:
path: /root/music/
spring:
datasource:
url: jdbc:mysql://124.223.222.249:3306/onlinemusic?useSSL=false&serverTimezone=UTC
password: 123456
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
servlet:
multipart.max-file-size: 15MB
multipart.max-request-size: 100MB
# 开启 MyBatis SQL 打印
logging:
level:
root: info
druid:
sql:
Statement: debug
com:
example: debug
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
debug: true
项目部署步骤:
-
在Linux中安装好关于Java 的环境依赖
jdk,Tomcat,还有就是MySQL数据库
。 -
创建一个music文件,用于存储上传的歌曲文件 相关命令
touch music
但是我们要记住此时一定要是在/root路径之下创建的music文件,因为我们在配置文件中已经说明了。 -
然后在idea中中的maven栏中进行package进行打包。
-
然后将打包好的文件上传到linux中
-
看看此时有没有那个进程占用了8080端口
- 使用
netstat -aup | grep 8080
- 然后使用
kill -9 进程Id
- 使用
-
然后使用命令
java -jar onlinemusic-0.0.1-SNAPSHOT.jar
进行项目部署之后我们就可以访问了,- 访问 124.223.222.249:8080/register.html
-
但是我们会发现此时我们一旦关闭这个linux页面,那么此时我们部署的项目就没了。
-
解决方法:创建一个用于记录程序执行的日志信息的文件 相关命令
touch log.log
然后使用 命令
nohup java -jar onlinemusic-0.0.1-SNAPSHOT.jar >> log.log &
进行项目步数,这样我们在操作项目的时候,相关的日志信息就展现在了log.log文件中。我们可以使用cat log.log
命令可以查看对应的日志信息。此时即便我么退出了linux,项目也是执行的。