前言:
这是我的第一个 SSM 项目 —— BY 音乐,所涉及到的技术:Spring、SpringBoot、SpringMVC、MyBatis、BCrypt 加密、自定义拦截器、HTML、CSS、JavaScrip、jquery、ajax ……
如项目有问题 or 改进方案随时下方留言,感谢支持 !!!
目录:
- 1、创建 springBoot 项目
- 2、设计数据库
- 3、在 springboot 项目中的配置问价中引入基本配置
- 4、实现登录功能
- 5、实现注册功能
- 6、实现修改密码功能
- 7、实现退出功能
- 8、实现上传音乐功能
- 9、实现搜索音乐功能(单个 or 全部)
- 10、实现播放音乐 + 功能
- 11、实现删除单个 / 多个功能
- 12、实现收藏功能
- 13、实现搜索收藏功能
- 14、实现删除收藏功能
- 15、配置拦截器
- 16、部署到云服务器
项目概览:
1、创建 springBoot 项目
⭐ 创建步骤如下:
2、设计数据库
⭐ 首先:数据库名命名为 ——> onlinemusic,此数据库中需要存储三张表分别是:
- user 表 : 存储用户信息
- music 表:存储音乐信息
- lovemusic 表:存储收藏音乐的信息
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);
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);
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);
3、在 springboot 项目中的配置问价中引入基本配置
⭐ 具体配置文件:(# 后的注释【部署到服务器上的配置】)
#配置数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&serverTimezone=UTC
#spring.datasource.username=root
#spring.datasource.password=@baiyang2001
#spring.datasource.driver-class-name=com.mysql.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
# 配置springboot日志调试模式是否开启
debug=true
# 设置打印日志的级别,及打印sql语句
#日志级别:trace,debug,info,warn,error
#基本日志
music.local.path=D:/music/
#music.local.path=/root/music/
logging.level.root=INFO
logging.level.com.example.onlinemusic.mapper=debug
#扫描的包:druid.sql.Statement类和frank包
logging.level.druid.sql.Statement=DEBUG
logging.level.com.example=DEBUG
4、实现登录功能
4.1、实现 model 层
⭐ 这里的 user 类中的属性名
和数据库中的字段名
保持一致
@Data
public class User {
private int id;
private String username;
private String password;
}
4.2、定义操作数据库的抽象方法
⭐ 创建实现操作的抽象方法:login
@Mapper
public interface UserMapper {
public User login(User loginUser);
}
4.3、在 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.musicserver.mapper.UserMapper">
<select id="login" resultType="com.example.musicserver.model.User">
select * from user where username = #{username} and password = #{password}
</select>
</mapper>
4.4、约定前后端接口
一、请求:
- 1、请求方式、请求路径。
- 2、响应的状态码(约定 0 为成功、-1 为失败)、登录提示、返回给前端的数据(数据类型为 json)
4.5、使用一个单独的类封装响应 —— ResponseBodyMessage
⭐ 这里使用的是泛型类:用于返回不同类型的 data 数据
@Data
public class ResponseBodyMessage<T>{
private int status;//状态码
private String message;//返回的信息【出错原因】
private T data;//返回个前端的数据
public ResponseBodyMessage(int status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
}
4.6、利用一个单独的类保存 session 中的 key
⭐ 单独使用一个类来表示 session 中的 key 防止 key 名字过长拼错,这样就可以在其他类直接调用不用手动拼写,减少错误发生
public class Constant {
public static final String USERINFO_SESSION_KEY = "USERINFO_SESSION_KEY";
}
4.7、实现登录框架 —— UserController + UserService
⭐ 先实现 UserService 实现具体的登录实现功能,这里使用到了 BCrypt 加密
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
public ResponseBodyMessage login(@RequestParam String username, @RequestParam String password,
HttpServletRequest request){
User user = userMapper.selectByName(username);
if(user == null){
return new ResponseBodyMessage<>(-1,"登录失败",user);
}else{
boolean flg = bCryptPasswordEncoder.matches(password,user.getPassword());
if(!flg){
return new ResponseBodyMessage<>(-1,"用户名或密码错误",user);
}
request.getSession().setAttribute(Constant.USERINFO_SESSION_KEY,user);
return new ResponseBodyMessage<>(0,"登录成功",user);
}
}
}
⭐UserController 只去实现方法调用即可:
@RequestMapping("/login")
public ResponseBodyMessage login(@RequestParam String username, @RequestParam String password,
HttpServletRequest request){
return userService.login(username,password,request);
}
4.8、有关密码加密问题
⭐ 对用户登录的密码进行加密:
①、MD5 加密
MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能得到原始的明文,即其过程不可逆; MD5是一种算法,可以从任何密码,短语或文本中生成32个字符的十六进制字符串例如,如果您的密码是“ qwerty”(不好的主意),则在数据库中您将拥 d8578edf8458ce06fbc5bb76a58c5ca4。但是虽然不可逆,但是不是说就是安全的。因为自从出现彩虹表后,这样的密码也"不安全"。
不安全的原因:
- 暴力攻击速度很快
- 字典表很大
- 碰撞
更安全的做法是加盐或者长密码等做法,让整个加密的字符串变的更长,破解时间变慢。密码学的应用安全,是建立在破解所要付出的成本远超出能得到的利益上的 。这里我们介绍加盐的做法:盐是在每个密码中加入一些单词来变成一个新的密码,存入数据库当中
⭐ 给密码加盐、可以是静态盐、也可以是动态盐(使用当前账户创建日期作为盐值这样就能保证盐值看起来是随机的),下面进行静态固定盐值的一个演示:
⭐需要引入 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>
/**
* @Author GaoBo
* @Description:
*/
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 inputPass
* @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"));
}
}
⭐不管运行多少次,这个密码是固定的。因为这里没有用随机盐值。当密码长度很大,盐值也是随机的情况下,密码的强度也加大了。破解成本也增加了。
②、Bcrypt加密数据
Bcrypt 就是一款加密工具,可以比较方便地实现数据的加密工作。你也可以简单理解为它内部自己实现了随机加盐处理 。我们使用MD5加密,每次加密后的密文其实都是一样的,这样就方便了MD5通过大数据的方式进行破解。Bcrypt生成的密文是60位的。而MD5的是32位的。Bcrypt破解难度更大。
⭐在 pom.xml 中添加 Bcrypt 依赖:
<!-- 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>
⭐springboot 启动类添加:(项目中没有使用到 spring security 这个框架、只是使用到了该框架下的一个类)
@SpringBootApplication(exclude ={org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
⭐ 总结:
- 密码学的应用安全,是建立在破解所要付出的成本远超出能得到的利益上的 。
- 使用 BCrypt 相比于 MD5 加密更好的一点在于,破解的难度上加大
- BCrypt的破解成本增加了,导致系统的运行成本也会大大的增加 。
- 回到本质的问题,你的数据库中的数据价值如何?如果你是银行类型的,那么使用BCrypt是不错的,一般情况使用MD5加盐,已经够用了。
③、二者对比:
- BCrypt加密: 一种加盐的单向Hash,不可逆的加密算法,同一种明文(plaintext),每次加密后的密文都不一样,而且不可反向破解生成明文,破解难度很大。
- MD5加密: 是不加盐的单向Hash,不可逆的加密算法,同一个密码经过hash的时候生成的是同一个hash值,在大多数的情况下,有些经过md5加密的方法将会被破解。
- Bcrypt生成的密文是60位的。而MD5的是32位的。目前,MD5和BCrypt比较流行。相对来说,BCrypt比MD5更安全,但加密更慢。 虽然BCrpyt也是输入的字符串+盐,但是与MD5+盐的主要区别是:每次加的盐不同,导致每次生成的结果也不相同。无法比对!
⭐ postman 验证:
4.9、前端代码
⭐ 只展示 js 代码:
<script>
$(function(){
$("#submit").click(function(){
var username=$("#user").val();
var password=$("#password").val();
$.ajax({
url:"/user/login",//指定路径
data:{"username":username,"password":password},
type:"POST",
dataType:"json",//服务器返回数据为json
success:function (data) {
console.log(data);
if(data.status==0){
alert("登录成功!");
window.location.href="index.html";
}else{
alert("用户名密码错误");
$("#user").val("");
$("#password").val("");
}
}
})
})
})
5、实现注册功能
5.1、定义操作数据库的抽象方法
int insertUser(String username, String password);
5.2、在 UserMapper.xml 文件中实现具体的数据库操作语句
<insert id="insertUser">
insert into user (username,password) values (#{username},#{password})
</insert>
5.3、约定前后端接口
5.4、实现注册框架
⭐ 实现 service 层:
public ResponseBodyMessage<Boolean> register(@RequestParam String username, @RequestParam String password){
User user = userMapper.selectByName(username);
if(user == null){
password = bCryptPasswordEncoder.encode(password);
int ret = userMapper.insertUser(username,password);
if(ret == 1){
return new ResponseBodyMessage<>(0,"注册成功",true);
}else{
return new ResponseBodyMessage<>(-1,"注册失败",false);
}
}else{
return new ResponseBodyMessage<>(-1,"该账户已经存在",false);
}
}
⭐ 实现 controller 层:
@RequestMapping("/register")
public ResponseBodyMessage<Boolean> register(@RequestParam String username, @RequestParam String password){
return userService.register(username,password);
}
5.5、前端代码
$(function(){
$("#rsb").click(function(){
var username = $("#newUsername").val();
var password = $("#newPassword").val();
var repassword = $("#repassword").val();
if(password != repassword){
alert("两次输入的密码不一致");
$("#newPassword").val("");
$("#repassword").val("");
return;
}
$.ajax({
url:"/user/register",
type:"post",
data:{"username":username,"password":password},
dataType:"json",
success:function(val){
console.log(val);
if(val.data == true){
alert("注册成功,请进行登录");
window.location.href="login.html";
}else{
alert("该账户已存在");
$("#newUsername").val("");
$("#newPassword").val("");
$("#repassword").val("");
return;
}
}
});
})
})
6、修改密码功能
6.1、定义操作数据库的抽象方法
int updatePassword(String username, String password);
6.2、在 UserMapper.xml 文件中实现具体的数据库操作语句
<update id="updatePassword">
update user set password=#{password} where username = #{username}
</update>
6.3、约定前后端接口
6.4、实现修改密码框架
⭐ service 层,实现修改密码的功能:
public ResponseBodyMessage<Boolean> update(@RequestParam String username, @RequestParam String password){
User user = userMapper.selectByName(username);
if(user == null){
return new ResponseBodyMessage<>(-1,"当前用户不存在无法修改",false);
}else{
password = bCryptPasswordEncoder.encode(password);
int ret = userMapper.updatePassword(username,password);
if(ret == 1){
return new ResponseBodyMessage<>(0,"修改成功",true);
}else{
return new ResponseBodyMessage<>(-1,"无法修改",false);
}
}
}
⭐ 实现 controller 层,调用方法:
@RequestMapping("/updatepassword")
public ResponseBodyMessage<Boolean> update(@RequestParam String username, @RequestParam String password){
return userService.update(username,password);
}
6.5、前端代码
$(function(){
$("#usb").click(function(){
var username = $("#username").val();
var password = $("#password").val();
var newPassword = $("#newPassword").val();
if(password != newPassword){
alert("两次输入密码不一致");
$("#password").val("");
$("#newPassword").val("");
return;
}
$.ajax({
type:"post",
url:"/user/updatepassword",
data:{"username":username,"password":password},
dataType:"json",
success:function(val){
if(val.data == true){
alert("修改成功");
window.location.href = "login.html";
}else{
alert("修改失败");
$("#password").val("");
$("#newPassword").val("");
$("#username").val("");
return;
}
}
});
})
})
7、实现退出功能
7.1、实现退出框架
⭐ 删除服务器中存储的用户登录信息(session)后重定向到登录页面,先实现 service 层
public ResponseBodyMessage<Boolean> exit(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(false);
session.removeAttribute(Constant.USERINFO_SESSION_KEY);
response.sendRedirect("/login.html");
return new ResponseBodyMessage<>(0,"退出成功",true);
}
⭐再实现 controller 层实现方法的调用:
@RequestMapping("/exit")
public ResponseBodyMessage<Boolean> exit(HttpServletRequest request, HttpServletResponse response) throws IOException {
return userService.exit(request,response);
}
7.2、前端代码
⭐前端代码就是一个 a 标签:向目标服务器发送退出请求即可
<a href="/user/exit">
8、实现上传音乐功能
8.1、定义操作数据库的抽象方法
int insert(String title, String singer, String time, String url, int userId);
Music select(String title, String singer);
8.2、在 MusicMapper.xml 文件中实现具体的数据库操作语句
<insert id="insert">
insert into music(title,singer,time,url,userId) values(#{title},#{singer},#{time},#{url},#{userId})
</insert>
8.3、约定前后端交互接口
8.4、实现上传音乐框架 + MP3 文件校验
⭐先创建一个 music 实体类:
@Data
public class Music {
private int id;
private String title;
private String singer;
private String time;
private String url;
private int userId;
}
⭐上传音乐有一个注意事项:需要分为服务器上传和数据库上传
⭐ 提前先注入配置文件中的文件保存路径:
@Value("${music.local.path}")
private String SAVE_PATH;
①、服务器的上传(musicService 中完成)
先检查用户是否登录,如果为登录提示未登录,登录后再进行后续的判断:
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute(Constant.USERINFO_SESSION_KEY) == null){
System.out.println("没有登录 !!! ");
return new ResponseBodyMessage<>(-1,"请登录后上传",false);
}
先获取到文件的文件名 + 后缀名:
//获取文件名 xx.mp3
String fileNameAndType = file.getOriginalFilename();
System.out.println("fileName:" + fileNameAndType);
进行文件名的拼接:和保存路径拼接成完整的路径名:
String path = SAVE_PATH + fileNameAndType;
去 new 这个文件:
File dest = new File(path);
如果 dest 不存在,就创建一个文件夹,存储此路径的文件夹就使用其即可
if(!dest.exists()){
dest.mkdir();
}
上传文件到目标位置:
try {
file.transferTo(dest);
return new ResponseBodyMessage<>(0,"上传成功",true);
} catch (IOException e) {
e.printStackTrace();
}
如何判断上传的文件是 MP3 格式的
判断这个文件是不是 MP3 文件,每个文件都有自己的组成格式,可以通过代码判断上传的音频文件中总共的 128 字节中,有3 字节有一个 TAG 标签
具体实现:读取 file 中的数据、判断是否存在 “TAG” 标签:
InputStream is = file.getInputStream();
InputStreamReader isReader = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isReader);
//循环逐行读取
String line;
boolean flg = false;
while ((line = br.readLine()) != null) {
if(line.contains("TAG")){
flg = true;
}
}
br.close();
if(flg == false){
return new ResponseBodyMessage<>(-1, "文件格式错误", false);
}
②、数据库的上传
创建上传接口:
@Mapper
public interface MusicMapper {
//插入音乐
int insert(String title, String singer, String time, String url, int userId);
}
实现 sql 语句:
<insert id="insert">
insert into music(title,singer,time,url,userId) values(#{title},#{singer},#{time},#{url},#{userId})
</insert>
得到各个属性:
int index = fileNameAndType.lastIndexOf(".");
String title = fileNameAndType.substring(0, index);
User user = (User) session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
String url = "/music/get?path=" + title;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String time = simpleDateFormat.format(new Date());
进行插入:
int ret = 0;
ret = musicMapper.insert(title, singer, time, url, userId);
if (ret == 1) {
return new ResponseBodyMessage<>(0, "上传成功", true);
} else {
return new ResponseBodyMessage<>(-1, "上传失败", false);
}
⭐如果存在同名同歌手的情况下,不能进行插入:现在数据库中进行查询,同 singer 同 title 的歌曲有没有存在,如果存在就不进行插入:
<select id="select" resultType="com.example.musicserver.model.Music">
select * from music where title = #{title} and singer = #{singer}
</select>
Music music = musicMapper.select(title,singer);
if(music != null){
return new ResponseBodyMessage<>(-1, "已有同名同歌手的歌曲", false);
}
⭐最后在controller 中进行调用:
@RequestMapping("/upload")
public ResponseBodyMessage<Boolean> insertMusic(@RequestParam String singer,
@RequestParam(value = "filename") MultipartFile file,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
return musicService.insertMusic(singer,file,request,response);
}
8.5、前端代码
<form action="/music/upload" method="post" enctype="multipart/form-data">
<div class="login-dialog">
<div>
<span style="margin-left: 165px;">文件上传</span>
<input type="file" name="filename" style="margin-left: 100px;margin-top: 15px;">
</div>
<div class="row">
<span style="margin-left: 90px;margin-top: 20px;">歌手名</span>
<label><input type="text" id="username" name="singer" placeholder="请输入歌手名" style="margin-top: 20px;"></label>
</div>
<div class="row">
<input type="submit" id="submit" value="确认上传" style="margin-left: 30px;">
</div>
</div>
</div>
</form>
9、实现搜索音乐功能(单个 or 全部)
9.1、定义操作数据库的抽象方法
⭐ 两种查询,第一种默认没有参数是查询所有音乐,第二种是进行模糊查询,查询带关键字的歌曲:
List<Music> findMusic();
List<Music> findMusicByName(String name);
9.2、在 UserMapper.xml 文件中实现具体的数据库操作语句
<select id="findMusic" resultType="com.example.musicserver.model.Music">
select * from music;
</select>
<select id="findMusicByName" resultType="com.example.musicserver.model.Music">
select * from music where title like concat('%',#{name},'%')
</select>
9.3、约定前后端交互接口
9.4、实现搜索音乐框架
⭐ 在 MusicService 中进行实现:两种查询音乐的方式
@RequestMapping("findmusic")
public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String name){
List<Music> music = null;
if(name == null){
music = musicMapper.findMusic();
}else {
music = musicMapper.findMusicByName(name);
}
return new ResponseBodyMessage<>(0,"查找成功",music);
}
⭐ 最后在 MusicController 中调用方法:
@RequestMapping("/findmusic")
public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String name){
return musicService.findMusic(name);
}
9.5、前端代码
⭐ 这里查询音乐(无论是多个还是单个都是需要动态去进行拼接的),所以这里采取 js 的原生拼接方法,去动态生成音乐列表的音乐:(两种 load 方式一个是有名字、一个没有名字)
$(function(){
load();
});
function load(musicname){
$.ajax({
url:"/music/findmusic",
data:{"name":musicname},
dataType:"json",
type:"get",
success:function(obj){
console.log(obj);
var data = obj.data;
var s = '';
for(var i = 0; i < data.length; i++){
s += '<div class="d-block d-md-flex podcast-entry bg-white mb-5" data-aos="fade-up">';
s += '<img src="images/R-C.jfif" class="image">';
s += '<div class="text">';
s += '<input type="checkbox" class="checkbox" style="margin-left: 710px;" id="'+data[i].id+'">';
s += '<h3 class="font-weight-light">' + data[i].title +'</h3>';
s += '<div class="text-white mb-3"><span class="text-black-opacity-05"><small>' + data[i].singer + '</small><span class="sep">/</span>' + DateFormat(data[i].time)+'</small></span></div>';
s += '<input type="button" class="btn btn-primary" style="margin-right: 20px;" value="收藏音乐" onclick="collectMusic(\''+ data[i].id + '\')">';
s += '<input type="button" class="btn btn-primary" style="margin: 20px;" value="删除音乐" onclick="deleteMusic(\'' + data[i].id + '\')">';
s += '<div class="player">';
s += '<audio id="player2" preload="none" controls style="max-width: 100%">';
s += '<source src="'+data[i].url + '.mp3' + '" type="audio/mp3">'
s+= '</audio></div></div></div>';
}
$('#MusicList').html(s);
}
});
}
$(function(){
$("#search").click(function(){
var name = $("#musictitle").val();
load(name);
});
10、 播放音乐模块设计
10.1、约定前后端接口
10.2、实现播放音乐框架
⭐借助 ResponseEntity<byte[]> 类实现:返回音乐的字节信息,前端传来文件的 path(文件名 + 文件类型),返回字节给前端,前端再进行解析进行音乐的播放
@RequestMapping("/get")
public ResponseEntity<byte[]> func(String path){
File file = new File(SAVE_PATH + "/" + path);
byte[] a = null;
try {
a = Files.readAllBytes(file.toPath());
if(a == null){
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(a);
} catch (IOException e) {
e.printStackTrace();
}
return ResponseEntity.badRequest().build();
}
⭐ 验证文件为音乐文件:
⭐ 插入一个文本文件(通过修改 MP3 后缀名使得 txt 文件成文 MP3 文件),可以发现是查找不到 TAG 标签的
10.3、前端代码
s += '<source src="'+data[i].url + '.mp3' + '" type="audio/mp3">'
11、实现删除单个 / 多个功能
11.1、定义操作数据库的抽象方法
int deleteById(Integer id);
Music selectById(Integer id);
11.2、在 MusicMapper.xml 文件中实现具体的数据库操作语句
<delete id="deleteById">
delete from music where id = #{id};
</delete>
11.3、约定前后端交互接口
11.4、实现删除单个 / 多个框架
①、删除单个:
⭐ 先查询在数据库中有没有需要删除的歌曲 id:先判断需要删除的音乐是否存在
⭐ 在 MusicService 类里进行具体的实现:(注意这里删除,收藏音乐也是要跟着一起进行删除的)
@RequestMapping("delete")
public ResponseBodyMessage<Boolean> deleteMusicById(@RequestParam String id){
int id1 = Integer.parseInt(id);
Music music = musicMapper.selectById(id1);
//检测要删除的 音乐 是否存在
if(music == null){
return new ResponseBodyMessage<>(-1,"没有此音乐,无法删除",false);
}
//进行数据库删除
int ret = 0;
ret = musicMapper.deleteById(id1);
if(ret == 1){
//数据库删除后 再进行服务器的删除
File file = new File(SAVE_PATH + "/" + music.getTitle());
if(file.delete()){
return new ResponseBodyMessage<>(0,"服务器删除成功",true);
}else{
return new ResponseBodyMessage<>(0,"服务器删除失败",true);
}
}else{
return new ResponseBodyMessage<>(-1,"删除失败",false);
}
}
⭐ 在 MusicController 类中调用方法:
@RequestMapping("/delete")
public ResponseBodyMessage<Boolean> deleteMusicById(@RequestParam String id){
return musicService.deleteMusicById(id);
}
②、删除多个:(删除多个音乐是需要传入多个 id 进行删除,需要使用到一个 id 的集合,遍历这个集合、进行上面的单个删除操作,最后进行判断是否删除了约定集合中的 id 个数)
public ResponseBodyMessage<Boolean> deleteSelMusic(@RequestParam("id[]") List<Integer> id){
int sum = 0;
for (int i = 0; i < id.size(); i++) {
Music music = musicMapper.selectById(id.get(i));
if(music == null){
return new ResponseBodyMessage<>(-1,"没有此音乐,无法删除",false);
}
//进行数据库删除
int ret = 0;
ret = musicMapper.deleteById(id.get(i));
if(ret == 1){
//数据库删除后 再进行服务器的删除
File file = new File(SAVE_PATH + "/" + music.getTitle() + ".mp3");
if(file.delete()){
sum += ret;
loveMusicMapper.deleteLoveMusicBymusicId(music.getId());
}else{
return new ResponseBodyMessage<>(0,"批量删除失败",true);
}
}else{
return new ResponseBodyMessage<>(-1,"批量删除失败",false);
}
}
if(sum == id.size()){
return new ResponseBodyMessage<>(0,"服务器删除成功",true);
}else{
return new ResponseBodyMessage<>(0,"批量删除失败",false);
}
}
⭐ 在 MusicController 中进行调用:
@RequestMapping("/deleteSel")
public ResponseBodyMessage<Boolean> deleteSelMusic(@RequestParam("id[]") List<Integer> id){
return musicService.deleteSelMusic(id);
}
11.5、前端代码
①、删除单个的前端代码:
function deleteMusic(obj){
console.log(obj);
$.ajax({
url:"/music/delete",
type:"post",
data:{"id" : obj},
dataType:"json",
success:function(val){
console.log(val);
if(val.data == true){
alert("删除成功,重新加载当前页面");
window.location.href = "index.html";
}else{
alert("删除失败")
}
}
});
}
①、删除多个的前端代码:(需要遍历复选框),需要等待 load 加载完毕再可以进行删除,需要使用到 when,删除多选是一个单独的按钮,当点击删除多选后触发一个点击时间,把复选框勾住的 id 都放到一个数组里面
$.when(load).done(function(){
$("#delete").click(function(){
var id = new Array();
var i = 0;
$("input:checkbox").each(function(){
if($(this).is(":checked")){
id[i] = $(this).attr("id");
i++;
}
});
console.log(id);
$.ajax({
url:"/music/deleteSel",
type:"post",
data:{"id":id},
dataType:"json",
success:function(obj){
if(obj.data == true){
alert("批量删除成功");
window.location.href = "index.html";
}else{
alert("批量删除失败");
}
}
});
});
});
});
12、实现收藏音乐的功能
12.1、定义操作数据库的抽象方法
⭐ 三个方法:查询收藏音乐是否在表中,如果在收藏列表进行收藏删除操作,如果不在进行收藏插入操作:
Music findLoveMusicByMusicIdAndUserId(int userId, int musicId);
boolean insertLoveMusic(int userId, int musicId);
boolean deleteLoveMusic(int userId, int musicId);
12.2、在 LoveMusicMapper.xml 文件中实现具体的数据库操作语句
<select id="findLoveMusicByMusicIdAndUserId" resultType="com.example.musicserver.model.Music">
select * from lovemusic where music_id = #{musicId} and user_id = #{userId}
</select>
<insert id="insertLoveMusic">
insert into lovemusic (user_id,music_id) values(#{userId},#{musicId})
</insert>
<delete id="deleteLoveMusic">
delete from lovemusic where music_id = #{musicId} and user_id = #{userId}
</delete>
12.3、约定前后端接口
12.4、实现收藏框架
⭐ 在 LoveMusicService 中实现:
public ResponseBodyMessage<Boolean> insertLoveMusic(@RequestParam String id, HttpServletRequest request){
int musicId = Integer.parseInt(id);
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute(Constant.USERINFO_SESSION_KEY) == null){
return new ResponseBodyMessage<>(-1,"当前尚未登录",false);
}
User user = (User) session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
Music music = loveMusicMapper.findLoveMusicByMusicIdAndUserId(userId,musicId);
if(music != null){
boolean flg = loveMusicMapper.deleteLoveMusic(userId,musicId);
if(flg){
return new ResponseBodyMessage<>(1,"取消收藏成功",true);
}else{
return new ResponseBodyMessage<>(-1,"取消收藏失败",false);
}
}else{
boolean ret = loveMusicMapper.insertLoveMusic(userId,musicId);
if(!ret){
return new ResponseBodyMessage<>(-1,"当前歌曲收藏失败",false);
}else{
return new ResponseBodyMessage<>(1,"当前歌曲成功收藏",true);
}
}
}
⭐ LoveMusicController 中进行调用:
@RequestMapping("/likemusic")
public ResponseBodyMessage<Boolean> insertLoveMusic(@RequestParam String id, HttpServletRequest request){
return loveMusicService.insertLoveMusic(id,request);
}
⭐postman 验证:
12.5、前端代码
function collectMusic(obj){
$.ajax({
type:"post",
url:"/lovemusic/likemusic",
data:{"id":obj},
dataType:"json",
success:function(val){
if(val.data == true){
alert("收藏成功");
window.location.href = "index.html";
}else{
console.log(val);
alert("收藏失败");
}
}
});
}
13、实现搜索收藏功能
13.1、定义操作数据库的抽象方法
List<Music> findLoveMusicByName(String musicName, int userId);
List<Music> findLoveMusic(int userId);
13.2、在 LoveMusicMapper.xml 文件中实现具体的数据库操作语句
⭐ 需要进行一个多表联合查询:
<select id="findLoveMusicByName" resultType="com.example.musicserver.model.Music">
select m.* from lovemusic lm,music m where m.id = lm.music_id and lm.user_id = #{userId} and title like concat('%',#{musicName},'%')
</select>
<select id="findLoveMusic" resultType="com.example.musicserver.model.Music">
select m.* from lovemusic lm,music m where m.id = lm.music_id and lm.user_id = #{userId}
</select>
13.3、约定前后端交互接口
13.4、实现搜索收藏框架
public ResponseBodyMessage<List<Music>> findLoveMusic(@RequestParam(required = false) String musicName, HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute(Constant.USERINFO_SESSION_KEY) == null){
return new ResponseBodyMessage<>(-1,"当前用户尚未登录",null);
}
User user = (User) session.getAttribute(Constant.USERINFO_SESSION_KEY);
int id = user.getId();
List<Music> music = null;
if(musicName == null){
music = loveMusicMapper.findLoveMusic(id);
}else{
music = loveMusicMapper.findLoveMusicByName(musicName,id);
}
return new ResponseBodyMessage<>(1,"查询成功",music);
}
13.5、前端代码
⭐和音乐列表页代码基本一致:
$(function(){
load();
});
function load(musicname){
$.ajax({
url:"/lovemusic/findlovemusic",
data:{"musicName":musicname},
dataType:"json",
type:"get",
success:function(obj){
console.log(obj);
var data = obj.data;
var s = '';
for(var i = 0; i < data.length; i++){
s += '<div class="d-block d-md-flex podcast-entry bg-white mb-5" data-aos="fade-up">';
s += '<img src="images/R-C.jfif" class="image">';
s += '<div class="text">';
// s += '<input type="checkbox" class="checkbox" style="margin-left: 525px;" id="'+data[i].id+'">';
s += '<h3 class="font-weight-light">' + data[i].title +'</h3>';
s += '<div class="text-white mb-3"><span class="text-black-opacity-05"><small>' + data[i].singer + '</small><span class="sep">/</span>' + DateFormat(data[i].time)+'</small></span></div>';
s += '<input type="button" class="btn btn-primary" style="margin-bottom: 20px;" value="移除收藏" οnclick="deleteMusic(\'' + data[i].id + '\')">';
s += '<div class="player">';
s += '<audio id="player2" preload="none" controls style="max-width: 100%">';
s += '<source src="'+data[i].url + '.mp3' + '" type="audio/mp3">'
s+= '</audio></div></div></div>';
}
$('#MusicList').html(s);
}
});
}
$(function(){
$("#search").click(function(){
var name = $("#musictitle").val();
load(name);
});
});
14、实现删除收藏功能
14.1、实现删除收藏功能
int deleteLoveMusicBymusicId(int musicId);
14.2、在 LoveMusicMapper.xml 文件中实现具体的数据库操作语句
<delete id="deleteLoveMusicBymusicId">
delete from lovemusic where music_id = #{musicId}
</delete>
14.3、约定前后端交互接口
⭐ 和之前的删除一致,这里不过多展示
14.4、实现删除收藏框架
public ResponseBodyMessage<Boolean> deletelovemusicById(@RequestParam String id,HttpServletRequest request){
int musicId = Integer.parseInt(id);
//没有session不创建
HttpSession httpSession = request.getSession(false);
if(httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
System.out.println("没有登录!");
return new ResponseBodyMessage<>(-1,"没有登录",false);
}
User user = (User)httpSession.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
boolean flg = loveMusicMapper.deleteLoveMusic(userId,musicId);
if(flg){
return new ResponseBodyMessage<>(1,"取消收藏成功",true);
}else{
return new ResponseBodyMessage<>(-1,"取消收藏失败",false);
}
}
⭐ 在 LoveMusicService 中调用:
@RequestMapping("/deletelovemusic")
public ResponseBodyMessage<Boolean> deletelovemusicById(@RequestParam String id,HttpServletRequest request){
return loveMusicService.deletelovemusicById(id,request);
}
14.5、前端代码
function deleteMusic(obj){
console.log(obj);
$.ajax({
url:"/lovemusic/deletelovemusic",
type:"post",
data:{"id" : obj},
dataType:"json",
success:function(val){
console.log(val);
if(val.data == true){
alert("删除成功,重新加载当前页面");
window.location.href = "contact.html";
}else{
alert("删除失败")
}
}
});
}
15、配置拦截器
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(Constant.USERINFO_SESSION_KEY) != null){
System.out.println("登录成功");
return true;
}
response.sendRedirect("login.html");
return false;
}
}
⭐ 设置拦截规则:去除一些页面的 js 、 css 、还有一些不需要登录也可以进行访问的页面:
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
LoginInterceptor loginInterceptor = new LoginInterceptor();
registry.addInterceptor(loginInterceptor).addPathPatterns("/**")
//排除所有的JS
.excludePathPatterns("/js/**.js")
//排除images下所有的元素
.excludePathPatterns("/images/**")
.excludePathPatterns("/css/**.css")
.excludePathPatterns("/fronts/**")
.excludePathPatterns("/player/**")
.excludePathPatterns("/login.html")
//排除登录接口
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/register")
.excludePathPatterns("/user/updatepassword")
.excludePathPatterns("/modif.html");
}
}
16、部署到服务器
⭐ 首先需要修改两个配置:连接 Linux 服务器上的数据库、修改音乐的保存位置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=@baiyang2001
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
music.local.path=/root/music/
⭐ package 打包成 jar 包、拷贝到云服务下:
⭐ 把本地数据库初始化的 sql 文件内容拷贝到云服务上的数据库里
⭐ java -jar +你打包的 jar 包名字:运行你的 jar 文件,此时如果你关闭服务器项目通过外网 ip 就不能访问了。
⭐ 让项目在服务器后台运行,保证关闭服务器也能通过外网 ip 访问项目,通过以下指令:
nohup java -jar xxx.jar >> log.log &