[项目篇] 音乐播放器开发报告

文章目录

    • 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项目搭建:
[项目篇] 音乐播放器开发报告_第1张图片
[项目篇] 音乐播放器开发报告_第2张图片
[项目篇] 音乐播放器开发报告_第3张图片

二. 设计数据库表

  • user表(存储用户信息)

    1. 用户Id——userId
    2. 用户名——username
    3. 用户密码——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);
    
  • music表(存储音乐数据)

    1. 哪个用户上传的这首音乐,得到这个用户的Id ——userId
    2. 歌曲标题——title
    3. 歌手——singer
    4. 时间——time
    5. 歌曲Id——id
    6. 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);
    
  • lovemuisc表(存储用户收藏的音乐信息)

    1. 收藏音乐的id
    2. 收藏音乐在music表中的music_id
    3. 收藏这首歌的用户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. 登录

  1. 定义model层中的实体类:

    根据数据库中的user表下的字段,我们需要在实体类中 定义一个User类,该类中的属性有:userId,username,password。

    @Data
    public class User {
        public int id;
        public String username;
        public String password;
    }
    //使用注解@Data 可以是代码简化,@Data中有类中的getter,setter,构造方法等功能
    
  2. 前后端交互约定:[项目篇] 音乐播放器开发报告_第4张图片

  3. 定义操作数据库的抽象方法:

    创建一个mapper包,在包下创建一个UserMapper接口,在接口中定义一个login(String username,String password)的抽象方法。

    /**
     * 根据用户名查找用户
     */
    User findByName(String username);
    

    首先通过username判断数据库中有没有这个用户,如果没有那么就在返回体中说明该用户不存在。

    /**
     * 判断密码是否正确
     */
     User findUser(String username, String password);
    
  4. 在UserMapper.xml中实现具体的操作user数据库的操作语句:

    <select id="findByName" resultType="com.example.onlinemusic.model.User">
        select * from user where username = #{username}
    select>
    
  5. 使用一个类约定后端服务器向前端服务器发送的响应体(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;
       }
   }
  
  1. 实现前后端交互接口:

    @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一样,也是一个组合注解,

  2. 密码加密介绍

    • 使用MD5加密:

      MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能的带原始的明文,那么这个过程就是不可逆的,但是虽然是不可逆的,但是这种方法也存在这个风险,因为在后来因为彩虹表的出现,这种MD5加密之后的面就没有那么有保密性了。

      彩虹就是一个庞大的,针对各种可能的自核预先极端号哈希值的集合,不一定是针对MD5算法的,各种算法都有,现在的彩虹表都有100G以上的。

      不安全的原因:

      • 暴力攻击的速度很快
      • 字典表很大
      • 碰撞

      更安全的做法是加盐或者是把密码设置长一点,让加密的字符串变长,破解的时间就会变慢,密码破解要结合它解密之后带来的经济效益。

      我们这里就是一个小系统,不至于人家那彩虹表给你破解密码。

      MD5加密的简单用法:

      引入关于MD5的相关依赖:

      
      <dependency>
          <groupId>commons-codecgroupId>
          <artifactId>commons-codecartifactId>
      dependency>
      <dependency>
          <groupId>org.apache.commonsgroupId>
          <artifactId>commons-lang3artifactId>
          <version>3.9version>
      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位的,破解更难

      添加依赖:

      
      <dependency>
          <groupId>org.springframework.securitygroupId>
          <artifactId>spring-security-webartifactId>
      dependency>
      <dependency>
          <groupId>org.springframework.securitygroupId>
          <artifactId>spring-security-configartifactId>
      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
      加密的密码和错误的密码对比结果: false

      encode方法对用户密码进行加密

      matches方法:第一个参数,表示的是还没有加密的密码,第二个参数表示的是从数据中查询到的加密之后的密码

      总结:

      • 密码学的应用安全,是家里在破解所要付出的成本远超过得到的利益上的
      • 使用BCrypt相比于MD5加密更好在于,破解的难度上加大了
      • BCrypt的破解成本增加了,导致运行成本也大大的增加
      • 总之我们这里使用MD5就已经足够了
    • MD5和BCrypt之间的区别:

      • BCrypt加密:一种加盐的单向hash,不可逆的加密算法,同一种明文,每次加密后的密文都是不一样的,并且不能反向破解生成明文,破解难度很大
      • MD5加密:是不加盐的单向hash,不可逆的加密算法,同一个密码经过hash的时候生成同一个hash值,在大多数的情况下,有些进过MD5加密的方法会被破解
  3. 登录具体业务实现:

@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";
    }
    
  1. 使用postman测试
    [项目篇] 音乐播放器开发报告_第5张图片
    [项目篇] 音乐播放器开发报告_第6张图片

  2. 前端代码

    
    <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>
    

[项目篇] 音乐播放器开发报告_第7张图片

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进行测试;
[项目篇] 音乐播放器开发报告_第8张图片

[项目篇] 音乐播放器开发报告_第9张图片
[项目篇] 音乐播放器开发报告_第10张图片

前端代码:

<!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>

[项目篇] 音乐播放器开发报告_第11张图片

3.退出系统

因为退出系统的业务条件比较简单,我们此时在controller(程序调用接口层)进行实现

其实退出系统的业务非常简单:就是把服务器中记录用户信息的sessionId 对应的信息给删除就行。

@PostMapping("/logout")
public ResponseBodyMessage<Boolean> logout(HttpServletRequest request){
    //得到此时用户的登录态,设置登录态为空
    request.getSession().removeAttribute(USERINFO_SESSION_KEY);
    return new ResponseBodyMessage<>("退出登录",0,true);
}

使用postman 进行测试:
[项目篇] 音乐播放器开发报告_第12张图片
[项目篇] 音乐播放器开发报告_第13张图片
[项目篇] 音乐播放器开发报告_第14张图片

前端代码:

//退出登录
$("#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前后端交互约定

[项目篇] 音乐播放器开发报告_第15张图片

4.2上传文件业务逻辑:

首先用户在前端页面上传一个文件,然后经过前端传递给后端。

  1. 判断当前用户是否是登录状态
  2. 判断传来的文件是否在数据库中已经存在了,如果存在就提醒用户该文件已存在。
  3. 然后判断文件的类型,此处的判断文件类型不能只判断文件是否已.mp3结尾,我们要知道上传.mp3文件的标准是什么,其实就是在mp3文件中字节码文件中用一个标志位使用TAG 表示的,我们可以通过它判断文件的类型
  4. 然后就是把文件上传到服务器中
  5. 把文件添加到数据库中
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框架中处理文件上传的主要类。

主要的方法有:
[项目篇] 音乐播放器开发报告_第16张图片


	@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的文件:
[项目篇] 音乐播放器开发报告_第17张图片

上传后缀名是.mp3的文件,但是该文件的本质不是一个mp3格式
[项目篇] 音乐播放器开发报告_第18张图片

4.10.前端代码:

<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前后端交互约定

[项目篇] 音乐播放器开发报告_第19张图片

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进行测试

[项目篇] 音乐播放器开发报告_第20张图片

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 前后端交互约定

[项目篇] 音乐播放器开发报告_第21张图片

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抽象方法(业务框架)
  1. 使用传来的音乐id在数据库中找到该文件,如果该文件为空,那么就没有id指向的文件,那么直接返回-1

  2. 然后根据这个id,删除数据库music表中id对应的文件。

  3. 但是我们在删除服务器中文件的时候就要注意了,我们在数据库中可能不同的用户上传了同一首音乐,同时服务器中的该音乐文件只有一个。那么此时我们需要使用该文件的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进行测试

[项目篇] 音乐播放器开发报告_第22张图片

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 前后端交互约定

[项目篇] 音乐播放器开发报告_第23张图片

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进行测试

[项目篇] 音乐播放器开发报告_第24张图片

[项目篇] 音乐播放器开发报告_第25张图片
[项目篇] 音乐播放器开发报告_第26张图片
[项目篇] 音乐播放器开发报告_第27张图片

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 前后端交互约定

[项目篇] 音乐播放器开发报告_第28张图片

[项目篇] 音乐播放器开发报告_第29张图片

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进行测试

[项目篇] 音乐播放器开发报告_第30张图片
[项目篇] 音乐播放器开发报告_第31张图片

前端代码:

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 += '';
                s += ' +data[i].id+'"type="checkbox"> ';
                s += '' + data[i].title + '';
                s += '' + data[i].singer + '';
                s+=' ' +
                    '';
                s+=' ' +
                    ''+
                    '';
                s += '';
            }
            $("#list").html(s);//把拼接好的页面添加到info的id下
        }
    })
}

8. 添加收藏音乐

8.1前后端交互约定

[项目篇] 音乐播放器开发报告_第32张图片

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 进行测试

[项目篇] 音乐播放器开发报告_第33张图片

[项目篇] 音乐播放器开发报告_第34张图片

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 前后端交互约定

[项目篇] 音乐播放器开发报告_第35张图片

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 进行测试

[项目篇] 音乐播放器开发报告_第36张图片

[项目篇] 音乐播放器开发报告_第37张图片

[项目篇] 音乐播放器开发报告_第38张图片

9.8 前端代码

删除收藏页面中的音乐逻辑和主页中的逻辑一致,这里不过多叙述

10.查询收藏音乐(模糊查询)

10.1 前后端交互约定

[项目篇] 音乐播放器开发报告_第39张图片

@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进行测试

[项目篇] 音乐播放器开发报告_第40张图片

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

项目部署步骤:

  1. 在Linux中安装好关于Java 的环境依赖jdk,Tomcat,还有就是MySQL数据库

  2. 创建一个music文件,用于存储上传的歌曲文件 相关命令 touch music 但是我们要记住此时一定要是在/root路径之下创建的music文件,因为我们在配置文件中已经说明了。

  3. 然后在idea中中的maven栏中进行package进行打包。

  4. 然后将打包好的文件上传到linux中

  5. 看看此时有没有那个进程占用了8080端口

    • 使用 netstat -aup | grep 8080
    • 然后使用 kill -9 进程Id
  6. 然后使用命令 java -jar onlinemusic-0.0.1-SNAPSHOT.jar 进行项目部署之后我们就可以访问了,

    • 访问 124.223.222.249:8080/register.html
  7. 但是我们会发现此时我们一旦关闭这个linux页面,那么此时我们部署的项目就没了。

  8. 解决方法:创建一个用于记录程序执行的日志信息的文件 相关命令 touch log.log

    然后使用 命令 nohup java -jar onlinemusic-0.0.1-SNAPSHOT.jar >> log.log &进行项目步数,这样我们在操作项目的时候,相关的日志信息就展现在了log.log文件中。我们可以使用cat log.log命令可以查看对应的日志信息。此时即便我么退出了linux,项目也是执行的。

你可能感兴趣的:(项目,mybatis,java,数据库)