您现在的位置是:首页 >技术交流 >实现音乐播放器网站首页技术交流
实现音乐播放器
目录
核心功能
1. 登录
2. 上传音乐
3. 删除指定音乐
4. 批量删除选中的音乐
5. 查询你想要的音乐
6. 添加音乐至喜欢的列表
7. 移除喜欢的音乐
演示效果
在线体验:http://43.138.233.192:8080/login.html 用户名:biaoge 密码:123456
项目Gitee链接:JavaEE: spring的项目学习 - Gitee.com
创建项目
数据库设计
创建数据库musicserver
-- 数据库
drop database if exists `musicserver`;
create database if not exists `musicserver` character set utf8;
-- 使用数据库
use `musicserver`;
创建表user
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`password` varchar(255) NOT NULL
);
INSERT INTO user(username,password)
VALUES("bit","$2a$10$Bs4wNEkledVlGZa6wSfX7eCSD7wRMO0eUwkJH0WyhXzKQJrnk85li");
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
);
配置数据库和xml
#配置数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=你的用户名
spring.datasource.password=你的密码
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
#基本日志
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
登录模块设计
1 创建User类
package com.example.musicserver.model;
import lombok.Data;
@Data
public class User {
private int id;
private String username;
private String password;
}
2 创建对应的Mapper和Controller
2.1 创建接口UserMapper
@Mapper
public interface UserMapper {
User login(User loginUser);
}
2.2 创建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>
3 实现登录
3.1 登录的请求和响应设计
请求:
{
post,
/user/login
data:{username,password}
}
响应:
{
"status": 0,
"message": "登录成功",
"data": {
"id": xxxxx,
"username": xxxxxx,
"password": xxxxxxxx
}
}
响应体设计字段解释:
{
状态码,为0代表成功,负数代表失败
状态描述信息,描述此次请求成功或者失败的原因
返回的数据,请求成功后,需要给前端的数据信息
}
3.2 设计统一的响应体类工具类
创建tools包中创建ResponseBodyMessage类
@Data
public class ResponBodyMessage<T> {
private int status;//状态码
private String message;//状态描述信息
private T data;//返回的数据
public ResponBodyMessage(int status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
}
3.3 创建UserController类
package com.example.musicserver.controller;
@RestController//@ResponseBody + @Controller合在一起的作用
@RequestMapping("/user")//使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求
public class UserController {
@Autowired
private UserMapper userMapper;
@RequestMapping("/login")
public ResponBodyMessage<User> login(@RequestParam String username, @RequestParam String password,
HttpServletRequest request) {
User userLogin = new User();
userLogin.setUsername(username);
userLogin.setPassword(password);
User user = userMapper.login(userLogin);
if(user != null) {
System.out.println("登录成功!");
request.getSession().setAttribute("USERINFO_SESSION_KEY",user);
return new ResponBodyMessage<>(0, "登录成功!",userLogin);
}else {
System.out.println("登录失败!");
return new ResponBodyMessage<>(-1, "登录失败!", userLogin);
}
}
}
注解介绍:@RestController : @ResponseBody + @Controller 合在一起的作用。 @Controller 注解,表明了这个类是一个控制器类 ,@ResponseBody 表示方法的返回值直接以指定的格式写入 Http response body 中 。@RequestMapping :使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些 URL 请求@RequestParam :将请求参数绑定到你控制器的方法参数上 。如果这个参数是非必传的可以写为: @RequestParam(required = false) ,默认是true
3.4 使用postman验证登录功能
3.5 优化代码
request.getSession().setAttribute("USERINFO_SESSION_KEY",userInfo);
package com.example.musicserver.tools;
/**
* Greated with IntelliJ IDEA.
* Description:
* User: 26524
* Date: 2023-04-19
* Time: 12:14
*/
public class Constant {
public static final String USERINFO_SESSION_KEY = "USERINFO_SESSION_KEY";
}
request.getSession().setAttribute(Constant.USERINFO_SESSION_KEY,user);
3.6 验证登录
重新启动项目,使用postman验证登录是否可以成功。
4 BCrypt加密的原理
4.1 MD5加密
彩虹表:彩虹表就是一个庞大的、针对各种可能的字母组合预先计算好的哈希值的集合,不一定是针对 MD5 算法的,各种算法的都有,有了它可以快速的破解各类密码。越是复杂的密码,需要的彩虹表就越大,现在主流的彩虹表都是100G 以上。
不安全的原因:
- 暴力攻击速度很快
- 字典表很大
- 碰撞
参考链接https://md5.cc/news1.aspx
更安全的做法是加盐或者长密码等做法,让整个加密的字符串变的更长,破解时间变慢。密码学的应用安全,是建立在破解所要付出的成本远超出能得到的利益上的 。
<!-- 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>
在tools包下,新建MD5Util类
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("对用户输入密码进行第3次加密:"+inputPassToDbPass("123456", "1b2i3t4e"));
}
}
不管运行多少次,这个密码是规定的。因为这里没有用随机盐值。当密码长度很大,盐值也是随机的情况下,密码的强度也加大了。破解成本也增加了。
4.2 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>
(exclude =
{org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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);
//使用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);
}
}
输出结果:【每次运行结果不一致,但是都能匹配成功】
解析:
encode 方法:对用户密码进行加密matches 方法:参数一,待检验的未加密的密码 。参数二:从数据库中查询出的加密后密码
总结:
- 密码学的应用安全,是建立在破解所要付出的成本远超出能得到的利益上的 。
- 使用BCrypt相比于MD5加密更好的一点在于,破解的难度上加大
- BCrypt的破解成本增加了,导致系统的运行成本也会大大的增加 。
- 回到本质的问题,你的数据库中的数据价值如何?如果你是银行类型的,那么使用BCrypt是不错的,一般情况使用MD5加盐,已经够用了。
【总结】:
参考链接:https://blog.csdn.net/muyimo/article/details/118811514
BCrypt 加密: 一种加盐的单向 Hash ,不可逆的加密算法,同一种明文( plaintext ),每次加密后的密文都不一 样,而且不可反向破解生成明文,破解难度很大。MD5 加密: 是不加盐的单向 Hash ,不可逆的加密算法,同一个密码经过 hash 的时候生成的是同一个 hash 值,在大多数的情况下,有些经过md5 加密的方法将会被破解。Bcrypt 生成的密文是 60 位的。而 MD5 的是 32 位的。目前, MD5 和 BCrypt 比较流行。相对来说, BCrypt 比 MD5 更安全,但加密更慢。 虽然 BCrpyt 也是输入的字符串加盐,但是与 MD5加 盐的主要区别是:每次加的盐不同,导致每次生成的结果也不相同。无法比对!
5 加密登录实现
5.1 数据库插入数据
5.2 UserMapper类新增方法
@Mapper
public interface UserMapper {
User login(User userLogin);
//username用户名是唯一的
User selectByName(String username);
}
5.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.demo.model.User">
select * from user where username=#{username} and password=#{password}
</select>
<select id="selectByName" resultType = "com.example.demo.model.User">
select * from user where username=#{username}
</select>
</mapper>
5.4 修改UserController类
@RestController//@ResponseBody + @Controller合在一起的作用
@RequestMapping("/user")//使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求
public class UserController {
@Autowired
private UserMapper userMapper;
@Autowired//在自动装配之前,需要完成注入,我们再AppConfig中进行注入
private BCryptPasswordEncoder bCryptPasswordEncoder;
@RequestMapping("/login")
public ResponBodyMessage<User> login(@RequestParam String username, @RequestParam String password,
HttpServletRequest request) {
User user = userMapper.selectByName(username);
if(user != null) {
boolean flg = bCryptPasswordEncoder.matches(password,user.getPassword());
if (!flg) {
return new ResponBodyMessage<>(-1, "用户名或者密码错误!", user);
}
System.out.println("登录成功!");
request.getSession().setAttribute(Constant.USERINFO_SESSION_KEY,user);
return new ResponBodyMessage<>(0, "登录成功!",user);
}else {
System.out.println("登录失败!");
return new ResponBodyMessage<>(-1, "用户名或者密码错误!", user);
}
}
}
5.5 创建包config,新建AppConfig类
@Configuration
public class AppConfig {
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
注解介绍:@Configuration :表明当前类是一个配置类,被注解的类内部包含有一个或多个被 @Bean 注解的方法,用于构建bean定义,初始化 Spring 容器。@Bean 注解:用于告诉方法,产生一个 Bean 对象,然后这个 Bean 对象交给 Spring 管理。产生这个 Bean 对象的方法Spring 只会调用一次,随后这个 Spring 将会将这个 Bean 对象放在自己的 IOC 容器中。SpringIOC 容器管理一个或者多个 bean ,这些 bean 都需要在 @Configuration 注解下进行创建,在一个方法上使用@Bean注解就表明这个方法需要交给 Spring 进行管理。
5.6 spring-boot启动类注解
@SpringBootApplication(exclude =
{org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
5.7 验证加密登录
使用postman进行登录验证,加密功能是否正常。
上传音乐模块设计
1 上传音乐的接口设计
请求:
{
post,
/music/upload
{singer,MultipartFile file},
}
响应:
{
"status": 0,
"message": "上传成功!",
"data": true
}
import lombok.Data;
@Data
public class Music {
private int id;
private String title;
private String singer;
private String url;
private String time;
private int userid;
}
2 创建MusicController类
在controller包下创建MusicController
@RestController
@RequestMapping("/music")
public class MusicController {
@Value("${music.local.path}")
private String SAVE_PATH;
@RequestMapping(value = "/upload")
public ResponBodyMessage<Boolean> insertMusic(@RequestParam String singer, @RequestParam("filename")
MultipartFile file, HttpServletRequest req, HttpServletResponse resp) throws IOException {
//没有session不创建
HttpSession httpSession = req.getSession(false);
if(httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
System.out.println("没有登录!");
return new ResponBodyMessage<>(-1,"没有登录!",false);
}
String filenameAndType = file.getOriginalFilename();//xxx.mp3
System.out.println("filenameAndType--->>>>>>>>>>>>>>>>>"+filenameAndType);
String path = SAVE_PATH +"\"+filenameAndType;
File dest = new File( path);
System.out.println("dest:=>" + dest.getPath());
if(!dest.exists()) {
dest.mkdirs();
}
try {
file.transferTo(dest);//上传文件到目标
return new ResponBodyMessage<>(0,"上传成功!",true);
} catch (IOException e) {
e.printStackTrace();
}
return new ResponBodyMessage<>(-1,"上传失败!",false);
}
}
- 使用@Value("${music.local.path}"),获取到配置文件当中的值。不建议中文路径。
#音乐上传后的路径 music.local.path=C:/work/local/music
- MultipartFile类,在org.springframework.web.multipart包当中,是Spring框架中处理文件上传的主要类。
主要方法介绍:
方法名
|
作用
|
String getOriginalFileName()
|
获取的是文件的完整名称,包括文件名称
+
文件拓展名。
|
String getContentType()
|
获取的是文件的类型,注意是文件的类型,不是文件的拓展名
|
boolean isEmpty()
|
用来判断传入的文件是否为空,如果为空则表示没有传入任何文件
|
long getSize()
|
获取文件的大小,单位是字节
|
void transferTo(File dest)
|
将接收到的文件传输到给定目标路径
|
3 如何判断上传的文件是mp3
由上图结构可知,每个Frame都由帧头和数据部分组成。我们来看每个帧头的数据格式。
ID3V1部分
参考链接:
4 实现数据库上传
package com.example.musicserver.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MusicMapper {
int insert(String title, String singer, String time, String url,int userid);
}
<?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.MusicMapper">
<insert id="insert">
insert into music(title,singer,time,url,userid) values(#{title},#{singer},#{time},#{url},#{userid})
</insert>
</mapper>
@Resource
private MusicMapper musicMapper;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestTime {
public static void main(String[] args) {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
String time = sf.format(new Date());
System.out.println("当前的时间: "+time);
SimpleDateFormat sf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time1 = sf1.format(new Date());
System.out.println("当前的时间: "+time1);
}
}
运行结果:
当前的时间: 2023-04-25
当前的时间: 2023-04-25 18:25:01
@RequestMapping("/upload")
public ResponBodyMessage<Boolean> insertMusic(@RequestParam String singer,
@RequestParam("filename") MultipartFile file,
HttpServletRequest request,
HttpServletResponse response) {
//检查是否登录了
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
System.out.println("没有登录!");
return new ResponBodyMessage<>(-1, "请登陆后上传!", false);
}
//获取文件名
String filename = file.getOriginalFilename();//xxx.mp3
System.out.println("fileNameAndType: " + filename);
//上传路径
String path = SAVE_PATH + "/"+filename;
//创建目录
File dest = new File(path);
if(!dest.exists()) {
System.out.println("没有这个目录");
dest.mkdir();
}
//上传文件
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
return new ResponBodyMessage<>(-1, "上传失败!", false);
}
//进行数据库上传
int index = filename.lastIndexOf(".");
String title = filename.substring(0, index);
//获取id
User user = (User)session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userid = user.getId();
//获取url
String url = "music/get?path="+title;
//获取time
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
String time = sf.format(new Date());
System.out.println("当前的时间: "+time);
try {
int ret = 0;
ret = musicMapper.insert(title,singer,time,url,userid);
if(ret == 1) {
//后面完善前端页面后,上传成功会跳转到音乐列表页面
//response.sendRedirect("/list.html");
return new ResponBodyMessage<>(0, "数据库上传成功!",true);
}else {
return new ResponBodyMessage<>(-1, "数据库上传失败!",false);
}
}catch (BindingException e) {
dest.delete();
return new ResponBodyMessage<>(-1, "数据库上传失败!",false);
}
}
5 验证整体文件上传
结果:
6 总结
6.1 MultipartFile类
参考链接:https://www.jianshu.com/p/e3d798c906cd
方法名
|
作用
|
String getOriginalFileName()
|
获取的是文件的完整名称,包括文件名称
+
文件拓展名。
|
String getContentType()
|
获取的是文件的类型,注意是文件的类型,不是文件的拓展名
|
boolean isEmpty()
|
用来判断传入的文件是否为空,如果为空则表示没有传入任何文件
|
long getSize()
|
获取文件的大小,单位是字节
|
void transferTo(File dest)
|
将接收到的文件传输到给定目标路径
|
6.2 如何判断是不是音乐文件?
6.3 相同的音乐是否可以上传成功?如何处理?
播放音乐模块设计
1 请求响应设计
请求:
{
get,
/music/get?path=xxx.mp3
}
响应:
{
音乐数据本身的字节信息
}
@RequestMapping("/get")
public ResponseEntity<byte[]> get(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();
}
- Files.readAllBytes(String path) : 读取文件中的所有字节,读入内存 ,参数path是文件的路径
- ResponseEntity
//这个方法若被调用的话,返回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方法
@RequestMapping("/get")
public ResponseEntity<byte[]> func() {
byte[] a = {97,98,99,100};
//return ResponseEntity.internalServerError().build();
return ResponseEntity.notFound().build();
//return ResponseEntity.ok(a);
}
参考链接:
https://blog.csdn.net/imaginehero/article/details/102810792?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1.topblog&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1.topblog&utm_relevant_index=1- https://www.jianshu.com/p/1238bfb29ee1
- https://blog.csdn.net/qq_43317193/article/details/100109136
2 测试请求是否收到对应响应
可以明显看到有TAG标签
删除音乐模块设计
删除单个音乐
1 请求响应设计
请求:
{
post,
/music/delete,
id
}
响应:
{
"status": 0,
"message": "删除成功!",
"data": true
}
2 代码实现
/**
* 根据ID删除音乐
* @param musicId
* @return
*/
int deleteMusicById(int musicId);
/**
* 根据ID查询音乐
* @param id
* @return
*/
Music findMusicById(int id);
<delete id="deleteMusicById" parameterType="java.lang.Integer">
delete from music where id=#{id}
</delete>
<select id="findMusicById" resultType="com.example.musicserver.model.Music">
select * from music where id=#{id}
</select>
/**
* 删除单个音乐
* @param id
* @param request
* @return
*/
@RequestMapping("/delete")
public ResponBodyMessage<Boolean> deleteMusicById(@RequestParam String id,
HttpServletRequest request) {
//1.先检查这个音乐是不是存在?
int iid = Integer.parseInt(id);
//2.如果存在要进行删除
Music music = musicMapper.findMusicById(iid);
if(music == null) {
return new ResponBodyMessage<>(-1, "没有你要找的音乐!", false);
}else {
//2.1删除数据库
int ret = musicMapper.deleteMusicById(iid);
if(ret == 1) {
//后面会增加这个,删除音乐的同时删除收藏音乐
//loveMusicMapper.deleteLoveMusicByMusicId(iid);
//2.2删除服务器上的数据
int index = music.getUrl().lastIndexOf("=");
String filename = music.getUrl().substring(index+1);
File file = new File(SAVE_PATH+"/"+filename+".mp3");
System.out.println("当前的路径:" + filename);
if(file.delete()) {
return new ResponBodyMessage<>(0, "服务器删除成功!", true);
}else {
return new ResponBodyMessage<>(-1, "服务器删除失败!", false);
}
}else {
return new ResponBodyMessage<>(-1, "数据库中的音乐没有删除成功!", false);
}
}
}
3 验证结果
批量删除选中的音乐
1 请求响应设计
请求:
{
post,
/music/deleteSel,
data:{"id":id}
}
响应:
{
"status": 0,
"message": "批量删除成功",
"data": true
}
2 代码实现
新增方法deleteSelMusic:
@RequestMapping("/deleteSel")
public ResponBodyMessage<Boolean> deleteSelMusic(@RequestParam("id[]") List<Integer> id) {
int sum = 0;
for(int i = 0; i < id.size(); i++) {
Music music = musicMapper.findMusicById(id.get(i));
if(music == null) {
System.out.println("没有这个id的音乐");
return new ResponBodyMessage<>(-1, "没有你要删除的音乐!", false);
}
//2.1删除数据库
int ret = musicMapper.deleteMusicById(id.get(i));
if(ret == 1) {
//后面会增加这个,删除音乐的同时删除收藏音乐
//loveMusicMapper.deleteLoveMusicByMusicId(id.get(i));
//2.2删除服务器上的数据
int index = music.getUrl().lastIndexOf("=");
String filename = music.getUrl().substring(index+1);
File file = new File(SAVE_PATH+"/"+filename+".mp3");
System.out.println("当前的路径:" + filename);
if(file.delete()) {
sum+=ret;
}else {
return new ResponBodyMessage<>(-1, "服务器删除失败!", false);
}
}else {
return new ResponBodyMessage<>(-1,"数据库删除失败!",false);
}
}
if(sum == id.size()) {
System.out.println("整体删除成功!");
return new ResponBodyMessage<>(0, "音乐删除成功!", true);
}else {
System.out.println("整体删除成功!");
return new ResponBodyMessage<>(-1, "音乐删除失败!",false);
}
}
3 验证结果
查询音乐模块设计
1 请求响应模块设计
- 支持模糊查询
- 支持传入参数为空
请求:
{
get,
/music/findmusic,
data:{musicName:musicName},
}
响应:【不给musicName传参】
{
"status": 0,
"data": [
{
"id": 64,
"title": "G_E_M_ 邓紫棋 - 错过不错",
"singer": "zjb",
"url": "music/get?path=G_E_M_ 邓紫棋 - 错过不错",
"time": "2023-04-25",
"userid": 1
},
{
"id": 65,
"title": "陈奕迅 - 让我留在你身边",
"singer": "zjb",
"url": "music/get?path=陈奕迅 - 让我留在你身边",
"time": "2023-04-25",
"userid": 1
},
{
"id": 66,
"title": "G_E_M_ 邓紫棋 - 另一个童话",
"singer": "zjb",
"url": "music/get?path=G_E_M_ 邓紫棋 - 另一个童话",
"time": "2023-04-25",
"userid": 1
},
{
"id": 68,
"title": "广东爱情故事",
"singer": "zjb",
"url": "music/get?path=广东爱情故事",
"time": "2023-04-25",
"userid": 1
},
{
"id": 69,
"title": "凉凉",
"singer": "zjb",
"url": "music/get?path=凉凉",
"time": "2023-04-25",
"userid": 1
}
],
"message": "查询到所有音乐!"
}
2 代码实现
/**
* 根据歌曲名字,查询音乐
* @param musicName
* @return
*/
List<Music> findMusicByMusicName(String musicName);
/**
* 查询所有的音乐
* @return
*/
List<Music> findMusic();
<select id="findMusicByMusicName" resultType="com.example.musicserver.model.Music">
select * from music where title like concat('%',#{musicName},'%')
</select>
<select id="findMusic" resultType="com.example.musicserver.model.Music">
select * from music
</select>
select * from music where title like concat('%',#{musicName},'%')
@RequestMapping("/findmusic")//(required=false)可以不传入参数
public ResponBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String musicName) {
List<Music> musicList = null;
if (musicName == null) {
//默认查询全部的音乐
musicList = musicMapper.findMusic();
}else {
musicList = musicMapper.findMusicByName(musicName);
}
return new ResponBodyMessage<>(0, "查询到所有音乐!", musicList);
}
3 验证结果
喜欢/收藏音乐模块设计
添加音乐至喜欢的列表模块设计
1 请求响应模块设计
请求:
{
post,
/lovemusic/likeMusic
data: id//音乐id
}
响应:
{
"status": 0,
"message": "点赞音乐成功",
"data": true
}
2 代码实现
实现LoveMusicMapper接口,收藏/喜欢音乐功能:
1、需要查询此次收藏音乐是否之前收藏过,收藏过则不能添加
@Mapper
public interface LoveMusicMapper {
/**
* 检查是否已经收藏过该音乐
* @param userId
* @param musicId
* @return
*/
Music findLoveMusicByUserIdAndMusicId(int userId, int musicId);
/**
* 点赞/收藏音乐
* @param userId
* @param musicId
* @return
*/
boolean insertLoveMusic(int userId, int musicId);
}
<?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.LoveMusicMapper">
<!--根据用户ID和音乐ID查询收藏的音乐 -->
<select id="findLoveMusicByUserIdAndMusicId" resultType="com.example.musicserver.model.Music">
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>
</mapper>
@RestController
@RequestMapping("lovemusic")
public class LoveMusicController {
@Autowired
LoveMusicMapper loveMusicMapper;
@RequestMapping("/likeMusic")
public ResponBodyMessage<Boolean> likeMusic(@RequestParam String id,
HttpServletRequest request) {
int musicId = Integer.parseInt(id);
System.out.println("musicId: " + musicId);
//没有session不创建
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
System.out.println("没有登录!");
return new ResponBodyMessage<>(-1, "没有登录!", false);
}
User user = (User)session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
//查询当前用户是否收藏过该音乐
Music music = loveMusicMapper.findLoveMusicByUserIdAndMusicId(userId, musicId);
if(music != null) {
return new ResponBodyMessage<>(-1, "该音乐已经收藏过了!",false);
}else {
boolean effer = loveMusicMapper.insertLoveMusic(userId, musicId);
if(effer) {
return new ResponBodyMessage<>(0, "收藏成功!", true);
}else {
return new ResponBodyMessage<>(-1, "收藏失败!",false);
}
}
}
}
3 验证结果
查询喜欢的音乐模块设计
1 请求响应设计
此处查询需要满足几个功能:
请求:
{
get,
/lovemusic/findlovemusic,
data:{musicName:musicName}
}
{
"status": 0,
"data": [
{
"id": 77,
"title": "凉凉",
"singer": "zjb",
"url": "music/get?path=凉凉",
"time": "2023-04-26",
"userid": 1
},
{
"id": 78,
"title": "G_E_M_ 邓紫棋 - 错过不错",
"singer": "zjb",
"url": "music/get?path=G_E_M_ 邓紫棋 - 错过不错",
"time": "2023-04-26",
"userid": 1
},
{
"id": 79,
"title": "G_E_M_ 邓紫棋 - 倒数",
"singer": "zjb",
"url": "music/get?path=G_E_M_ 邓紫棋 - 倒数",
"time": "2023-04-26",
"userid": 1
},
{
"id": 80,
"title": "陈奕迅 - 让我留在你身边",
"singer": "zjb",
"url": "music/get?path=陈奕迅 - 让我留在你身边",
"time": "2023-04-26",
"userid": 1
}
],
"message": "查询到所有的歌曲信息"
}
2 代码实现
/**
* 如果没有传入具体的歌曲名,显示当前用户收藏的所有音乐
* @param userId
* @return
*/
List<Music> findLoveMusicByUserId(int userId);
/**
* 根据某个用户的ID和歌曲名称查询,某个用户收藏的音乐
* @param musicName
* @param userId
* @return
*/
List<Music> findLoveMusicBykeyAndUID(String musicName, int userId);
<select id="findLoveMusicByUserId" 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>
<select id="findLoveMusicBykeyAndUID" 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>
@RequestMapping("/findlovemusic")
public ResponBodyMessage<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) {
System.out.println("没有登录!");
return new ResponBodyMessage<>(-1, "请登陆后查找!", null);
}
User user = (User)session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
List<Music> musicList = null;
if(musicName == null) {
musicList = loveMusicMapper.findLoveMusicByUserId(userId);
}else {
musicList = loveMusicMapper.findLoveMusicBykeyAndUID(musicName, userId);
}
System.out.println(musicList);
return new ResponBodyMessage<>(0, "查询到所有的歌曲信息", musicList);
}
3 验证结果
移除喜欢的音乐模块设计
1 请求响应设计
请求:
{
post,
/lovemusic/deletelovemusic,
data:{id:id}
}
响应:
{
"status": 0,
"message": "取消收藏成功!",
"data": true
}
2 代码实现
/**
* 移除自己收藏的音乐,但是不是删除音乐本身,只是从数据库中删除了记录而已
* @param userId
* @param musicId
* @return
*/
int deleteLoveMusic(int userId,int musicId);
<delete id="deleteLoveMusic" parameterType="java.lang.Integer">
delete from lovemusic where user_id=#{userId} and music_id=#{musicId}
</delete>
@RequestMapping("/deletelovemusic")
public ResponBodyMessage<Boolean> deleteLoveMusic(@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) {
System.out.println("没有登录!");
return new ResponBodyMessage<>(-1, "请登陆后删除!", null);
}
User user = (User)session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
int ret = loveMusicMapper.deleteLoveMusic(userId, musicId);
if (ret == 1) {
return new ResponBodyMessage<>(0, "取消收藏成功!", true);
}else {
return new ResponBodyMessage<>(-1, "取消收藏失败!",false);
}
}
3 验证结果
删除音乐完善
/**
* 当删除库中的音乐的时候,同步删除lovemusic中的数据
* @param musicId
* @return
*/
int deleteLoveMusicByMusicId(int musicId);
<delete id="deleteLoveMusicByMusicId" parameterType="java.lang.Integer">
delete from lovemusic where music_id=#{musicId}
</delete>
@Autowired
private LoveMusicMapper loveMusicMapper;
重写MusicController.java文件中的deleteMusicById和deleteSelMusic两个方法
添加loveMusicMapper.deleteLoveMusicByMusicId();
@RequestMapping("/delete")
public ResponBodyMessage<Boolean> deleteMusicById(@RequestParam String id,
HttpServletRequest request) {
//1.先检查这个音乐是不是存在?
int iid = Integer.parseInt(id);
//2.如果存在要进行删除
Music music = musicMapper.findMusicById(iid);
if(music == null) {
return new ResponBodyMessage<>(-1, "没有你要找的音乐!", false);
}else {
//2.1删除数据库
int ret = musicMapper.deleteMusicById(iid);
if(ret == 1) {
/* -------> */ loveMusicMapper.deleteLoveMusicByMusicId(iid);
//2.2删除服务器上的数据
int index = music.getUrl().lastIndexOf("=");
String filename = music.getUrl().substring(index+1);
File file = new File(SAVE_PATH+"/"+filename+".mp3");
System.out.println("当前的路径:" + filename);
if(file.delete()) {
return new ResponBodyMessage<>(0, "服务器删除成功!", true);
}else {
return new ResponBodyMessage<>(-1, "服务器删除失败!", false);
}
}else {
return new ResponBodyMessage<>(-1, "数据库中的音乐没有删除成功!", false);
}
}
}
@RequestMapping("/deleteSel")
public ResponBodyMessage<Boolean> deleteSelMusic(@RequestParam("id[]") List<Integer> id) {
int sum = 0;
for(int i = 0; i < id.size(); i++) {
Music music = musicMapper.findMusicById(id.get(i));
if(music == null) {
System.out.println("没有这个id的音乐");
return new ResponBodyMessage<>(-1, "没有你要删除的音乐!", false);
}
//2.1删除数据库
int ret = musicMapper.deleteMusicById(id.get(i));
if(ret == 1) {
//2.2删除服务器上的数据
/* -------> */ loveMusicMapper.deleteLoveMusicByMusicId(id.get(i));
int index = music.getUrl().lastIndexOf("=");
String filename = music.getUrl().substring(index+1);
File file = new File(SAVE_PATH+"/"+filename+".mp3");
System.out.println("当前的路径:" + filename);
if(file.delete()) {
sum+=ret;
// return new ResponBodyMessage<>(0, "服务器删除成功!", true);
}else {
return new ResponBodyMessage<>(-1, "服务器删除失败!", false);
}
}else {
return new ResponBodyMessage<>(-1,"数据库删除失败!",false);
}
}
前端页面实现
tips: 做减法比做加法更容易 .例如: http://www.cssmoban.com/preview/index.html?url=http://demo.mxyhn.xyz:8020/cssthemes6/podca-gh-pages/index.html&id=10615&tid=20125072756579
将网页模板解压缩, 拷贝到项目的 static目录中.
此处提供本项目前端代码:
JavaEE: spring的项目学习 - Gitee.com
配置拦截器
在config包下创建LoginInterceptor类
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
HttpSession httpSession = request.getSession(false);
if(httpSession!=null &&
httpSession.getAttribute(Constant.USERINFO_SESSION_KEY)!=null) {
//此时是登录状态
return true;
}
return false;
}
}
在AppConfig类中实现WebMvcConfigurer接口,再添加重写的addInterceptors方法
/**
* 添加拦截器,将自定义拦截器加入到系统配置
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//1、配置拦截规则
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");
}
- addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意方法(也就是所有方法)。
- excludePathPatterns:表示需要排除的 URL。
项目部署
1. 将数据库在服务器上重新进行建表等操作
2. 修改项目中的路径,数据库密码等,匹配服务器即可
注意上传文件目录
#本地目录
music.local.path=C:/work/local/music1
#云服务器下
#music.local.path=/root/music
数据库用户密码
#云服务器上的用户名和密码
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
3. 使用java -jar xxxx.jar 启动项目【前台运行的方式】
4. http://你的公网地址:8080/login.html
5. 后台运行springboot项目:nohup java -jar xxx.jar >> log.log &
nohup :后台运行项目的指令使用 >> log.log 将运行的日志记录到 log.log 中& 表示 一直运行
6.进行功能的检查
7.如果项目端口被占用:
- 查看端口状态:netstat -anp | grep "端口号"
- 然后kill -9 18506杀掉进程
- 重新部署
8.如果想部署多个项目:
打开你的云服务器:
在项目的application.properties下添加
server.port=9090
重新部署
就此结束,
文章过长,
如果发现其中错误,
希望即使指正,
谢谢