目录
本篇博客带大家做一个网页版的音乐播放器项目
技术栈主要是SSM框架, 基本的创建项目就不写了, 直接上正题.
一. 数据库设计
我们的音乐播放器, 需要有登录注册功能, 所以 user 表不可少
其次, 还需要一个音乐库, 作为我们的首页内容
最后, 每个用户都有自己喜欢的音乐, 这是最后一个表
用户和音乐是多对多的关系, 用户可以喜欢多个音乐, 音乐可以被很多人喜欢
user 表
User 表基础就是一个自增主键id, 一个用户名和一个密码
这里密码我们需要加密, 所以给较长的空间.
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("zhangsan","$2a$10$Bs4wNEkledVlGZa6wSfX7eCSD7wRMO0eUwkJH0WyhXzKQJrnk85li");
music 表
音乐同样要一个自增编号, 还需要名称, 歌手, 上传时间, 用于找到音乐的存储位置
最后标识一下这首音乐上传的用户
DROP TABLE IF EXISTS `music`;
CREATE TABLE `music` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`title` varchar(50) NOT NULL,
`singer` varchar(30) NOT NULL,
`time` varchar(13) NOT NULL,
`url` varchar(1000) NOT NULL,
`userid` int(11) NOT NULL
);
lovemusic表
我们还需要一个表来表示用户喜欢的音乐
DROP TABLE IF EXISTS `lovemusic`;
CREATE TABLE `lovemusic` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`music_id` int(11) NOT NULL
);
二. 配置数据库
在 application.properites 文件下来配置文件
填入以下内容:
#配置数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/musicserver?characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=989811
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.musicsplayer.mapper=debug
#扫描的包:druid.sql.Statement类和frank包
logging.level.druid.sql.Statement=DEBUG
logging.level.com.example=DEBUG
三. 登录模块设计
创建User类
创建一个model包, 存放User
@Data 是 lombok 的一个注解, 加上这个我们就不用写Getter和Setter方法了
@Data
public class User {
private int id;
private String username;
private String password;
}
创建Mapper
创建一个mapper包, 用来存放数据库操作接口
初步内容:
@Mapper
public interface UserMapper {
User login(User user);
}
再在resources下创建mybatis包, 放置mybatis的配置文件和sql语句
配置内容:
<?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.musicplayer.mapper.UserMapper">
<select id="login" resultType="com.example.musicplayer.model.User">
select * from user where username=#{username} and password=#{password}
</select>
</mapper>
四. 实现登录逻辑
用户请求报文
建controller包:
写一个登陆的逻辑:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;
@RequestMapping("/login")
public void login(@RequestParam String username,
@RequestParam String password){
User userLogin = new User();
userLogin.setUsername(username);
userLogin.setPassword(password);
User user = userMapper.login(userLogin);
//如果两个数据都能查到, 就说明用户存在, 登录成功
if (user != null){
System.out.println("登陆成功!");
}else {
System.out.println("登陆失败!");
}
}
}
启动项目, 调试一下看能否登录成功
登录成功就说明没有问题了.
响应报文
先创建一个tools包,放我们的JSON响应报文类
在ResponseBodyMessage类中, 就是我们的响应报文
@Data
public class ResponseBodyMessage <T> {
private int status;//状态码(1成功, 0失败)
private String message;//返回的信息
private T data;//返回给前端的数据
public ResponseBodyMessage(int status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
}
接着我们的登录的逻辑就可以改了,返回值改为返回报文
并在登录成功时创建一个会话
为了以后方便调取这个会话,写一个静态属性包装一下会话名字:
public class Constant {
public static final String USERINFO_SESSION_KEY = "USERINFO_SESSION_KEY";
}
最后修改好的UserController:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;
@RequestMapping("/login")
public ResponseBodyMessage<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(Constant.USERINFO_SESSION_KEY, user);
return new ResponseBodyMessage<>(1, "登陆成功!", userLogin);
}else {
System.out.println("登陆失败!");
return new ResponseBodyMessage<>(0, "登陆失败!", userLogin);
}
}
}
我们还可以用 postman 抓个包:
密码加密
密码这块,我们肯定不能明文传输,否则抓个包就获取到了
我们这里采用 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>
接着在启动类的注解上加一行:
@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
public class MusicplayerApplication {
public static void main(String[] args) {
SpringApplication.run(MusicplayerApplication.class, args);
}
}
因为我们用BCrypt加密时,导入了一个spring-security框架
这个框架有自己的配置,我们需要忽略这些配置,所以启动时加上这一行
接下来开始进行BCrypt加密:
我们先添加一个查找用户名是否存在的方法, 因为密码被加密了
如果用户名存在我们就进行解密操作, 再匹配密码正确与否
@Mapper
public interface UserMapper {
User login(User user);
User selectByName(String username);
}
<select id="selectByName" resultType="com.example.musicplayer.model.User">
select * from user where username=#{username}
</select>
接下来开始对 UserController 类修改
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private UserMapper userMapper;
@RequestMapping("/login")
public ResponseBodyMessage<User> login(@RequestParam String username,
@RequestParam String password,
HttpServletRequest request){
User user = userMapper.selectByName(username);
if (user != null){
boolean flag = bCryptPasswordEncoder.matches(password, user.getPassword());
if (!flag){
return new ResponseBodyMessage<>(0, "用户名或密码错误!", user);
}
System.out.println("登陆成功!");
request.getSession().setAttribute(Constant.USERINFO_SESSION_KEY, user);
return new ResponseBodyMessage<>(1, "登陆成功!", user);
}else {
System.out.println("登陆失败!");
return new ResponseBodyMessage<>(0, "登陆失败!", user);
}
}
}
当然,为了方便起见,我们为加密类 BCryptPasswordEncoder 写一个注入:
创建 config 包,里面存放着很多 Bean
创建一个存放 Bean 的类,将加密类存放到 Spring 中
@Configuration
public class AppConfig {
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
这样以后需要使用该类时,直接注入即可。
接着使用Postman抓包测试:
五. 上传音乐模块
服务器上传
首先, 上传音乐肯定用 post
结束了用户模块, 我们需要一个音乐模块
@Data
public class Music {
private int id;
private String title;
private String singer;
private String time;
private String url;
private int userid;
}
接着写music的操作方法:
第一步: 用会话session检查是否登录
第二步: 指定文件保存路径, 并保存
@RestController
@RequestMapping("/music")
public class MusicController {
@Value("${music.local.path}")
private String SAVE_PATH;
@RequestMapping("/upload")
public ResponseBodyMessage<Boolean> insertMusic(String singer, @RequestParam("filename") MultipartFile file, HttpServletRequest request){
//检查是否登录
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(Constant.USERINFO_SESSION_KEY) == null){
return new ResponseBodyMessage<>(0, "请登录后上传", false);
}
String fileName = file.getOriginalFilename();// 获取文件名
System.out.println("文件名: " + fileName);
String path = SAVE_PATH + "/" + fileName;// 指定文件保存路径
File destFile = new File(path);
System.out.println("文件路径: " + destFile.getPath());
if (!destFile.exists()){
destFile.mkdirs();// 别少加了 s
}
try {
file.transferTo(destFile);// 保存文件到指定路径
return new ResponseBodyMessage<>(1, "上传成功", true);
} catch (IOException e) {
e.printStackTrace();
}
return new ResponseBodyMessage<>(0, "上传失败", false);
}
}
对于上面代码中的 SAVE_PATH, 我们可以写到配置文件中, 方便调用
#音乐上传后的路径
music.local.path=D:/player/musics/
数据库上传
涉及到数据库, 都用mybatis, 我们创建一个 MusicMapper 类
先写一个插入方法
@Mapper
public interface MusicMapper {
int insert(String title, String singer, String time, String url, String userid);
}
<insert id="insert">
insert into music(title, singer, time, url, userid)
values(#{title}, #{singer}, #{time}, #{url}, #{userid})
</insert>
获取除后缀名的文件名:
int index = fileName.lastIndexOf(".");
String title = fileName.substring(0, index);
获取userid:
User user = (User)session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userid = user.getId();
获取音乐url:
String url = "/music/get?path=" + title;
获取时间:
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String time = simpleDateFormat.format(new Date());
上传到数据库:
这里注意, 如果中间有问题没上传成功, 服务器需要把这个问题文件删除,
try {
int ret = 0;
ret = musicMapper.insert(title, singer, time, url, userid);
if (ret == 1){
//可以跳转到音乐列表界面
return new ResponseBodyMessage<>(1, "服务器上传成功", true);
}else {
return new ResponseBodyMessage<>(0, "服务器上传失败", false);
}
}catch (BindingException e){
destFile.delete();
e.printStackTrace();
return new ResponseBodyMessage<>(0, "服务器上传失败", false);
}
当然, 我们还能添加一个功能, 上传歌曲时, 我们可以判断是否重复上传了
在 MusicMapper 里添加一个select方法就行
Music music = musicMapper.selectSame(title, singer);
if (music != null){
return new ResponseBodyMessage<>(0, "请勿重复上传", false);
}
六. 播放音乐模块
播放音乐的格式采用GET, 直接在URL中体现: /get?path=xxx.mp3
@RequestMapping("/get")
public ResponseEntity<byte[]> play(String path){
File file = new File(SAVE_PATH + "/" + path);
byte[] plays = null;
try {
// 找到路径下的文件进行读取
plays = Files.readAllBytes(file.toPath());
// 若未获取文件, 返回一个错误请求
if (plays == null){
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(plays);// 正常播放
} catch (IOException e) {
e.printStackTrace();
}
return ResponseEntity.badRequest().build();
}
ResponseEntity 对象是Spring对请求响应的封装. 它继承了HttpEntity对象,包含了Http的响应码
(httpstatus)、响应头(header)、响应体(body)三个部分。
如果是歌曲, 在抓包时可以看到, 字节码文件内有 TAG
七. 删除音乐模块
删除单个音乐:
首先, 请求响应设计:
根据传输的 id 来删除音乐
服务器先查找是否有此音乐, 再操作数据库删除, 最后返回响应报文
接着设计Mapper, 首先要根据id查找是否有此书:
接着删除:
再写后端代码:
@RequestMapping("/delete")
public ResponseBodyMessage<Boolean> deleteMusicById(@RequestParam String id){
int deleteId = Integer.parseInt(id);//前端拿到的都是String类型的
Music deleteMusic = musicMapper.findById(deleteId);
if (deleteMusic == null){
//System.out.println("无此id的音乐");
return new ResponseBodyMessage<>(0, "删除失败, 没有您要删除的音乐", false);
}
int flag = musicMapper.deleteMusicById(deleteId);
if (flag == 0){
return new ResponseBodyMessage<>(0, "删除失败, 未知原因", false);
}
//别忘了删除本地文件
String title = deleteMusic.getTitle();
File file = new File(SAVE_PATH + "/" + title + ".mp3");
//System.out.println("当前路径:" + file.getPath());
if (file.delete()){
return new ResponseBodyMessage<>(1, "删除成功", true);
}
return new ResponseBodyMessage<>(0, "本地音乐删除失败", false);
}
最后用postman验证即可.
批量删除音乐
和删除单个音乐类似, 是不过传一个 id 数组进来删除
@RequestMapping("/deletes")
public ResponseBodyMessage<Boolean> deleteMusics(@RequestParam("id[]") List<Integer> id){
System.out.println("id[]" + id);
int sum = 0;
for (int i = 0; i < id.size(); i++) {
Music deleteMusic = musicMapper.findById(id.get(i));
if (deleteMusic == null){
//System.out.println("无此id的音乐");
return new ResponseBodyMessage<>(0, "删除失败, 没有您要删除的音乐", false);
}
int ret = musicMapper.deleteMusicById(id.get(i));
if (ret == 0){
return new ResponseBodyMessage<>(0, "数据库音乐删除失败", false);
}
String title = deleteMusic.getTitle();
File file = new File(SAVE_PATH + "/" + title + ".mp3");
if (file.delete()){
//这里不能return, 因为要删很多
sum += ret;
}else {
return new ResponseBodyMessage<>(0, "本地音乐删除失败", false);
}
}
if (sum == id.size()){
return new ResponseBodyMessage<>(1, "所选全部音乐删除成功", true);
}
return new ResponseBodyMessage<>(0, "本地音乐删除失败", false);
}
postman测试:
八. 查找音乐模块
查找的方式就是模糊查询
在参数为空时, 默认返回所有音乐
开始写Mapper:
一个是参数为空, 查找全部音乐:
另一个是用户传入以音乐名, 根据音乐名查找歌曲:
MusicController 逻辑:
@RequestMapping("/find")
public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String musicName){
List<Music> musicList = null;
if (musicName != null){
musicList = musicMapper.findMusicByName(musicName);
}else {
musicList = musicMapper.findMusic();
}
return new ResponseBodyMessage<>(0, "查询到了所有音乐", musicList);
}
postman验证:
九. 收藏音乐模块
协议设计:
收藏音乐逻辑:
查询此音乐是否被收藏过, 如果没有, 则在收藏表中插入此数据.
Mapper设计:
Controller设计:
@RequestMapping("/insert")
public ResponseBodyMessage<Boolean> insertLoveMusic(HttpServletRequest request,
@RequestParam String musicId) {
// 检查是否登录
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(Constant.USERINFO_SESSION_KEY) == null){
return new ResponseBodyMessage<>(0, "请登录后上传", false);
}
/*
注意, userId可以通过session获取, 不用前端传参
*/
User user = (User) session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
// 检查是否重复收藏
Music music = loveMusicMapper.findLoveMusic(userId, Integer.parseInt(musicId));
if (music != null){
return new ResponseBodyMessage<>(0, "请勿重复收藏", false);
}
// 插入音乐
if (loveMusicMapper.insertLoveMusic(userId, Integer.parseInt(musicId))){
return new ResponseBodyMessage<>(1, "收藏成功", true);
}
return new ResponseBodyMessage<>(0, "收藏失败", false);
}
postman验证:
十. 收藏音乐查询模块
此模块与第八块类似
同样需要模糊查询和传参为空返回全部音乐
Mapper设计:
由于lovemusic表内只有ID字段, 所以需要多表查询, 根据userId来查
<select id="findLoveMusicById" resultType="com.example.musicplayer.model.Music">
select m.* from lovemusic lm, music m where lm.music_id=m.id and user_id=#{userId}
</select>
<select id="findLoveMusicByNameAndId" resultType="com.example.musicplayer.model.Music">
select m.* from lovemusic lm, music m where lm.music_id=m.id and user_id=#{userId}
and m.title like concat('%', #{musicname}, '%')
</select>
Controller设计:
@RequestMapping("/findlovemusic")
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<>(0, "请登录后查找", null);
}
//获取UserId
User user = (User) session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
//检查是否传参
List<Music> musicList = null;
if (musicName == null){
musicList = loveMusicMapper.findLoveMusicById(userId);
}else {
musicList = loveMusicMapper.findLoveMusicByNameAndId(musicName, userId);
}
return new ResponseBodyMessage<>(1, "查询到了所有的歌曲信息", musicList);
}
最后postman验证:
十一. 取消收藏音乐模块
请求与响应模块:
Mapper设计:
Controller设计:
@RequestMapping("/deletelovemusic")
public ResponseBodyMessage<Boolean> deleteLoveMusic(
HttpServletRequest request,
@RequestParam String id){
// 检查是否登录
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(Constant.USERINFO_SESSION_KEY) == null){
return new ResponseBodyMessage<>(0, "请登录后查找", null);
}
//获取UserId
User user = (User) session.getAttribute(Constant.USERINFO_SESSION_KEY);
int userId = user.getId();
int musicId = Integer.parseInt(id);
boolean reg = loveMusicMapper.deleteLoveMusic(userId, musicId);
if (reg){
return new ResponseBodyMessage<>(1, "取消收藏音乐成功", true);
}
return new ResponseBodyMessage<>(0, "取消收藏音乐失败", false);
}
postman验证:
删除的一些细节
注意, 不仅有一个lovemusic表, 还有一个music总表
在删除总表的音乐后, lovemusic表内的数据也要删除
所以在此要更改一下:
首先, 添加一个根据音乐ID删除的逻辑:
更改MusicController:
if (file.delete()){
// 同步删除收藏音乐表内的数据
loveMusicMapper.deleteLoveMusicById(deleteId);
return new ResponseBodyMessage<>(1, "删除成功", true);
}
if (file.delete()){
loveMusicMapper.deleteLoveMusicById(id.get(i));
sum += ret;
}else {
return new ResponseBodyMessage<>(0, "本地音乐删除失败", false);
}
谢谢你能看到这, 一起加油ヽ( ̄ω ̄( ̄ω ̄〃)ゝ