当前位置: 首页 > news >正文

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

文章目录

    • 1. 项目描述:
    • 2. 项目上线展现:
    • 3. 项目具体实现:
      • 1. 登录
      • 2. 注册
      • 3.退出系统
      • 4.添加音乐
        • 4.1前后端交互约定
        • 4.2上传文件业务逻辑:
        • 4.3创建model包中的music类
        • 4.4在MusicMapper接口中,声明insertMusic抽象方法
        • 4.5在mybatis包中添加操作数据的SQL语句
        • 4.6 实现控制层controller中的接口方法
        • 4.7 在事务层中创建MusicService接口中的insertMusic抽象方法
        • 4.8 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
        • 4.9使用postman进行测试
        • 4.10.前端代码:
      • 5. 播放音乐
        • 5.1前后端交互约定
        • 5.2定义后端交互接口
        • 5.3 在MusicService中定义描述播放音乐的接口
        • 5.4在service包下的impl子包中实现接口中的getMusic抽象方法
        • 5.5ResponseEntiy类介绍
        • 5.6 播放音乐业务层实现逻辑
        • 5.7 使用postman进行测试
        • 5.8前端代码
      • 6. 删除音乐(单个删除,批量删除)
        • 6.1 单个删除
          • 6.1.1 前后端交互约定
          • 6.1.2 删除业务实现逻辑描述
          • 6.1.3 在MusicMapper中声明删除单个音乐的接口和根据id查找音乐的接口
          • 6.1.4 在mybatis包中添加操作数据的SQL语句
          • 6.1.5后端实现接口
          • 6.1.6 在业务层中创建MusicService接口中的deleteMusic抽象方法
          • 6.1.7 在业务层中service包中的impl子包中实现deleteMusic抽象方法(业务框架)
          • 6.1.8 使用postman进行测试
          • 6.1.9前端代码
        • 6.2 实现批量删除
          • 6.2.1 前后端交互约定
          • 6.2.2 实现后端交互接口(是否批量删除成功)
          • 6.2.3 批量删除音乐逻辑描述
          • 6.2.4业务层中创建MusicService接口中的deletePartMusic抽象方法
          • 6.2.5在业务层中service包中的impl子包中实现deletePartMusic抽象方法(业务框架)
          • 6.2.6使用postman进行测试
          • 6.2.7 前端代码
      • 7. 查询音乐(支持模糊匹配)
        • 7.1 前后端交互约定
        • 7.2在MusicMapper中声明查询音乐的接口
        • 7.3 在mybatis包中添加操作数据的SQL语句
        • 7.4 后端实现接口
        • 7.5 在业务层中service包中的impl子包中实现findMusic抽象方法(业务框架)
        • 7.6 使用postman进行测试
      • 8. 添加收藏音乐
        • 8.1前后端交互约定
        • 8.2 在model层下添加loveMuisic实体类
        • 8.3 添加收藏音乐的具体逻辑实现:
        • 8.4 在LoveMusicMapper中声明添加收藏音乐的接口
        • 8.5 在mybatis包中添加操作数据的SQL语句
        • 8.6 实现后端交互接口
        • 8.7 在业务层中创建LoveMusicService接口中的insertMusic抽象方法
        • 8.8 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
        • 8.9 使用postman 进行测试
        • 8.10 前端代码
      • 9. 删除收藏音乐
        • 9.1 前后端交互约定
        • 9.2 在LoveMusicMapper中声明删除收藏音乐的接口
        • 9.3 在mybatis包中添加操作数据的SQL语句
        • 9.4 实现后端交互接口
        • 9.5在业务层中创建LoveMusicService接口中的insertMusic抽象方法
        • 9.6 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)
        • 9.7 使用postman 进行测试
        • 9.8 前端代码
      • 10.查询收藏音乐(模糊查询)
        • 10.1 前后端交互约定
        • 10.2在LoveMusicMapper中声明删除收藏音乐的接口
        • 10.3在mybatis包中添加操作数据的SQL语句
        • 10.4在业务层中创建LoveMusicService接口中的findMusic抽象方法
        • 10.5 使用postman进行测试
      • 11.代码完善
      • 11.设置登录拦截器
      • 12.使用服务器部署上线

1. 项目描述:

主要业务:注册,登录,注销,新增,查询,删除,播放歌曲。

  • 在业务方面实现了基础的增删查改;
  • 基于BCrypt对用户所传的密码进行加密;
  • 实现了自定义登录拦截器;
  • 使FileputStream,FileReader 判断上传文件是否是.mp3类型的文件

技术选型:Java,Spring,SpringMVC,SpringBoot,AJAX,MySQL,MyBatis,html,css,Js,redis

2. 项目上线展现:

3. 项目具体实现:

一. SpringBoot项目搭建:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二. 设计数据库表

  • user表(存储用户信息)

    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. 前后端交互约定:在这里插入图片描述

  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;/*** 封装统一响应体*/@Datapublic 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 {@Resourceprivate 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的相关依赖:

      <!-- md5 依赖 -->
      <dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId>
      </dependency>
      <dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version>
      </dependency>
      
      public class MD5Util {public static final String str = "1z2h3o4u5j6i7n";public static String md5(String str){return DigestUtils.md5DigestAsHex(str.getBytes());}public static String intPutPassFromNewPass(String password){String s = "" + str.charAt(0) + str.charAt(5) + str + str.charAt(6) + str.charAt(2) + str.charAt(3);return md5(s);}public static void main(String[] args) {String s = intPutPassFromNewPass("123456");System.out.println(s);String s1 = intPutPassFromNewPass(s);System.out.println(s1);}
      }
      

      同一个明文密码,在进行两次加密之后得到的运行结果:

      568a03aca5deb1c401e2f9028d7fd150
      568a03aca5deb1c401e2f9028d7fd150

      我们可以看到两次加密的结果是一样的,并且每次运行这个加密程序得到的都是同一种结果。

      其实在我们的小系统中使用这中加密方式也是可以的,毕竟我们的系统应该不会有人破解密码码,一点利益价值都没有😁😁😁😁😁

    • 使用BCrypt加密:

      BCrypt也是一个加密方式,可以比较方便的实现数据的加密,我们可以简单的理解在这个类中的加密方法,有一种动态加盐的操作,我们使用的MD5加密,每次加密后的密文其实都是一样的,这样方便了MD5通过大量数据方式进行破解,我们这里的BCrypt生成的密文是60位的,MD5生成的密文是32位的,破解更难

      添加依赖:

      <!-- security依赖包 (加密)-->
      <dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId>
      </dependency>
      <dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId>
      </dependency>
      

      项目中没有使用到 spring security 这个框架、只是使用到了该框架下的一个类

      @SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
      public class OnlinemusicApplication {public static void main(String[] args) {SpringApplication.run(OnlinemusicApplication.class, args);}
      }
      
      public class BCryptTest {public static void main(String[] args) {
      //模拟从前端获得的密码String password = "123456";BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String newPassword = bCryptPasswordEncoder.encode(password);System.out.println("加密的密码为: "+newPassword);boolean same_password_result = bCryptPasswordEncoder.matches(password,newPassword);System.out.println("加密的密码和正确密码对比结果: "+same_password_result);boolean other_password_result = bCryptPasswordEncoder.matches("987654",newPassword);System.out.println("加密的密码和错误的密码对比结果: " + other_password_result);}
      }
      

      加密的密码为: $2a101010Xykg/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();@Autowiredprivate UserMapper userMapper;@Overridepublic 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测试
    在这里插入图片描述
    在这里插入图片描述

  2. 前端代码

    <!DOCTYPE html>
    <html>
    <head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>注册界面</title><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><link rel="stylesheet" href="css/bootstrap.css"><link href="iconfont/style.css" type="text/css" rel="stylesheet"><style>body {color: #fff;font-family: "微软雅黑";font-size: 14px;}.wrap1 {position: absolute;top: 0;right: 0;bottom: 0;left: 0;margin: auto}/*把整个屏幕真正撑开--而且能自己实现居中*/.main_content {background: url(images/main_bg.png) repeat;margin-left: auto;margin-right: auto;text-align: left;float: none;border-radius: 8px;}.form-group {position: relative;}.login_btn {display: block;background: #3872f6;color: #fff;font-size: 15px;width: 100%;line-height: 50px;border-radius: 3px;border: none;}.login_input {width: 100%;border: 1px solid #3872f6;border-radius: 3px;line-height: 40px;padding: 2px 5px 2px 30px;background: none;}.icon_font {position: absolute;bottom: 15px;left: 10px;font-size: 18px;color: #3872f6;}.font16 {font-size: 16px;}.mg-t20 {margin-top: 20px;}@media (min-width: 200px) {.pd-xs-20 {padding: 20px;}}@media (min-width: 768px) {.pd-sm-50 {padding: 50px;}}#grad {background: -webkit-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Safari 5.1 - 6.0 */background: -o-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Opera 11.1 - 12.0 */background: -moz-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Firefox 3.6 - 15 */background: linear-gradient(#4990c1, #52a3d2, #6186a3); /* 标准的语法 */}</style></head><body style="background:url(images/bg.jpg) no-repeat;"><div class="container wrap1" style="height:450px;"><h2 class="mg-b20 text-center">onlineMusic登录页面</h2><div class="col-sm-8 col-md-5 center-auto pd-sm-50 pd-xs-20 main_content"><p class="text-center font16">用户登录</p><div class="form-group mg-t20"><i class="icon-user icon_font"></i><input type="text" class="login_input" id="username" placeholder="请输入用户名"/></div><div class="form-group mg-t20"><i class="icon-lock icon_font"></i><input type="password" class="login_input" id="password" placeholder="请输入密码"/></div><input type="submit" class="login_btn" value="登录" id="submit"></div>
    </div>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script>$(function () {$("#submit").click(function () {//得到输入框中的输入内容let username = $("#username").val();let password = $("#password").val();//判断空值if (username.trim() === "" || password.trim() === "") {alert("输入内容不能为空!");return;}$.ajax({url:"/user/login",//指定路径data:{"username":username,"password":password},type:"POST",dataType:"json",//服务器返回数据为jsonsuccess:function (data) {console.log(data);if(data.status===1){alert("登录成功!");window.location.href="list.html";}else{alert("登录失败,账号或密码错误,请重试!");$("#message").text("账号或密码错误,请重试!");$("#user").val("");$("#password").val("");}}});})})
    </script>
    </body>
    </html>
    

在这里插入图片描述

2. 注册

主要实现思想:用户前端传来username和password,然后在业务层判断数据库中是否存在这个username,如果存在,那么直接返回false,否则就把用户添加到数据库中,返回true

❤️❤️在UserMapper接口中定义抽象方法register(String username,String password)****

/*** 注册用户信息*/
Boolean register(String username,String password);

❤️❤️在resource 资源包下中的UserMapper.xml中添加SQL语句(在数据库中添加用户)

 <insert id="register" >insert into user values(null,#{username},#{password});
</insert>

😁😁实现前后端交互接口中的register()方法,如果传来的两个参数为空,或者字符串的长度为0,那么就返回参数异常,如果在数据库查询这个注册用户已经存在了,那么就返回-1,如果注册失败就返回-2

    @PostMapping("/register")public ResponseBodyMessage<User> register(@RequestParam String username, @RequestParam String password) {//判断传入参数是否为空if (StringUtils.isAnyBlank(username, password)) {return new ResponseBodyMessage<>("参数异常",0,null);}int register = userService.register(username, password);if(register == -1){return new ResponseBodyMessage<>("该用户已存在",-1,null);}else if(register == -2){return new ResponseBodyMessage<>("注册失败",-2,null);}return new ResponseBodyMessage<>("注册成功",1,null);}

😁😁UserService中的register()抽象方法

/*** 注册*/
int register(String username,String password);

😁😁UserService包下的impl包下的UserServiceImpl类实现register方法


@Override
public int register(String username, String password) {//判断该用户在数据库中是否存在User user = userMapper.findByName(username);if(user != null){return -1;}//进行加密password = bCryptPasswordEncoder.encode(password);Boolean register = userMapper.register(username, password);if(!register){return -2;}return 1;
}

使用postman进行测试;
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

前端代码:

<!DOCTYPE html>
<html>
<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>注册界面</title><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><link rel="stylesheet" href="css/bootstrap.css"><link href="iconfont/style.css" type="text/css" rel="stylesheet"><style>body {color: #fff;font-family: "微软雅黑";font-size: 14px;}.wrap1 {position: absolute;top: 0;right: 0;bottom: 0;left: 0;margin: auto}/*把整个屏幕真正撑开--而且能自己实现居中*/.main_content {background: url(images/main_bg.png) repeat;margin-left: auto;margin-right: auto;text-align: left;float: none;border-radius: 8px;}.form-group {position: relative;}.login_btn {display: block;background: #3872f6;color: #fff;font-size: 15px;width: 100%;line-height: 50px;border-radius: 3px;border: none;}.login_input {width: 100%;border: 1px solid #3872f6;border-radius: 3px;line-height: 40px;padding: 2px 5px 2px 30px;background: none;}.icon_font {position: absolute;bottom: 15px;left: 10px;font-size: 18px;color: #3872f6;}.font16 {font-size: 16px;}.mg-t20 {margin-top: 20px;}@media (min-width: 200px) {.pd-xs-20 {padding: 20px;}}@media (min-width: 768px) {.pd-sm-50 {padding: 50px;}}#grad {background: -webkit-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Safari 5.1 - 6.0 */background: -o-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Opera 11.1 - 12.0 */background: -moz-linear-gradient(#4990c1, #52a3d2, #6186a3); /* Firefox 3.6 - 15 */background: linear-gradient(#4990c1, #52a3d2, #6186a3); /* 标准的语法 */}</style></head><body style="background:url(images/bg.jpg) no-repeat;"><div class="container wrap1" style="height:450px;"><h2 class="mg-b20 text-center">onlineMusic注册页面</h2><div class="col-sm-8 col-md-5 center-auto pd-sm-50 pd-xs-20 main_content"><p class="text-center font16">用户注册</p><div class="form-group mg-t20"><i class="icon-user icon_font"></i><input type="text" class="login_input" id="username" placeholder="请输入用户名"/></div><div class="form-group mg-t20"><i class="icon-lock icon_font"></i><input type="password" class="login_input" id="password" placeholder="请输入密码"/></div><div class="form-group mg-t20"><i class="icon-lock icon_font"></i><input type="password" class="login_input" id="confirmPassword" placeholder="请输入确认密码"/></div><input type="submit" class="login_btn" value="注册" id="submit"></div>
</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>$(function () {$("#submit").click(function () {//得到输入框中的输入内容let username = $("#username").val();let password = $("#password").val();let confirmPassword = $("#confirmPassword").val();//判断空值if (username.trim() === "" || password.trim() === "" || confirmPassword.trim() === "") {alert("输入内容不能为空!");return;}if (password !== confirmPassword) {alert("密码和确认密码要相同!")return;}$.ajax({url: '/user/register',data:{"username":username,"password":password},type:"POST",dataType:"json",//服务器返回数据为jsonsuccess:function (data) {console.log(data);if(data.status===1){alert("注册成功!");window.location.href="login.html";}else if(data.status === -1){alert("该用户已存在");}else{alert("注册失败");}}});})})
</script>
</body>
</html>

在这里插入图片描述

3.退出系统

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

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

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

使用postman 进行测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

前端代码:

//退出登录
$("#logout").click(function (){$.ajax({url: '/user/logout',type: 'post',success: function (result) {if (result) {alert("退出成功,返回登录页面");window.location.assign("login.html");} else {alert("退出失败");}}})
})

4.添加音乐

4.1前后端交互约定

在这里插入图片描述

4.2上传文件业务逻辑:

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

  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;            //音乐idpublic String title;      //音乐标题public String singer;     //歌手public String time;       //时间public String url;        //urlpublic 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框架中处理文件上传的主要类。

主要的方法有:
在这里插入图片描述

@Autowiredprivate 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文件 使用得到字节数组中的最后一段字节码,判断二进制字符串中是否存在TAGboolean 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//得到userIdUser user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);int userId = user.getId();//得到url 此处的url 用于播放String url = "/music/get?path=" + title;//得到时间SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");String time = simpleDateFormat.format(new Date());//如果此时的数据库存放文件失败,那么此时的服务器中的文件也应该消失没有try{int ret = musicMapper.insertMusic(title, singer, time, url, userId);//数据库存储失败if(ret != 1){return -2;}return 1;}catch (BindingException e){//如果在把文件信息添加到数据库的时候发生了异常,那么此时就把服务器中的文件也要删了//如果不删,那么此时服务器和数据库中的文件数据就不一致了file1.delete(); //删除服务器中的文件return -2;}}

1.判断用户是否登录


//检查此时用户是否登录HttpSession session = request.getSession(false);if (session == null || session.getAttribute(USERINFO_SESSION_KEY) == null) {return -1;}

2.判断删除的文件是否满足mp3文件格式:

我们不能是否文件的后缀来判断,某个文件是否是.mp3文件,因为谁知道那个老六会把其他类型的文件的后缀名改为.mp3文件。

其实每个文件都有自己的组成方式,在每个文件的尾部,长度为128字节,有一个.mp3公有的特点,就是有TAG标识

读取文件中的信息,把这些信息转化成为utf-8类型,然后进行逐行读取,判断读取的每一行中是否有"TAG"字段

InputStream 只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使

FileInputStream

转化字符集类型

InputStreamReader(InputStream in, Charset cs) 创建一个使用给定字符集的InputStreamReader

BufferedReader 从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取

//判断传出的文件是否是mp3文件 使用得到字节数组中的最后一段字节码,判断二进制字符串中是否存在TAG
boolean isMp3Type = false;
try {InputStream is = file.getInputStream();InputStreamReader isReader = new InputStreamReader(is, StandardCharsets.UTF_8);BufferedReader br = new BufferedReader(isReader);//循环逐行读取String line;while ((line = br.readLine()) != null) {if(line.contains("TAG")){isMp3Type = true;}}br.close();
} catch (IOException e) {e.printStackTrace();
}
if(!isMp3Type){return -3;
}

3.判断数据库中是否已经存在这个文件

使用getOriginalFilename方法得到这个文件的名字和类型,然后在得到"."的下标,然后再使用substring()方法得到0~.之间的字符串,这就是文件的具体名字,即title,然后根据这个title和singer判断数据库中是否存在这个singer所唱的title ,因为同一首歌会有许多人唱,所以使用singer和title在数据库中匹配。

String fileNameAndType = file.getOriginalFilename();
assert fileNameAndType != null;
int index = fileNameAndType.lastIndexOf(".");
String title = fileNameAndType.substring(0, index);
//判断数据库中是否存在和要即将添加的音乐重名并且歌手名相同,如果相同就是重复的歌曲,题型用户
Music music = musicMapper.findMusicNyTitleAndSinger(title, singer);
//歌曲已经上传过了
if(music != null){return 0;
}

4.如果此时在数据库中未找到这个音乐,那么此时就把文件上传到服务器中。

此时得到服务器中要存放文件的盘符路径 和 这个文件的文件名和后缀,拼接,判断这个文件路径是否已经存在,如果不存在,使用mkdir创建一个。然后使用transferTo()方法,把文件上传到指定目录。

//得到存放音乐文件的路径
//盘福路径 + 歌曲名称
String path = SAVE_PATH + fileNameAndType;
File file1 = new File(path);
//如果该目录不存在,那么就重新创建一个
if (!file1.exists()) {file1.mkdir();
}
try {//向指定目录中上传音乐file.transferTo(file1);
} catch (IOException e) {e.printStackTrace();//服务器存储失败return -2;
}

5.把文件相关内容写到数据库中

此时我们要把相关这个文件的 title(文件名),userId(上传文件的userId),time(上传文件的时间),singer(歌手),url传到数据库。

//得到userId
User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);
//根据已经登录的session信息得到此时用户的Id
int userId = user.getId();
//得到url 此处的url 用于播放
String url = "/music/get?path=" + title;
//得到时间
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String time = simpleDateFormat.format(new Date());
//如果此时的数据库存放文件失败,那么此时的服务器中的文件也应该消失没有
try{int ret = musicMapper.insertMusic(title, singer, time, url, userId);//数据库存储失败if(ret != 1){return -2;}return 1;
}catch (BindingException e){//如果在把文件信息添加到数据库的时候发生了异常,那么此时就把服务器中的文件也要删了//如果不删,那么此时服务器和数据库中的文件数据就不一致了file1.delete(); //删除服务器中的文件return -2;
}

4.9使用postman进行测试

同一个用户上传同一首歌曲是不能上传成功的

上传后缀名不是mp3的文件:
在这里插入图片描述

上传后缀名是.mp3的文件,但是该文件的本质不是一个mp3格式
在这里插入图片描述

4.10.前端代码:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>上传音乐</title>
</head>
<body>
<form action="/music/upload" method="post" enctype="multipart/form-data">上传文件:<input type="file" name="filename"><label>歌手名:<input type="text" name="singer" placeholder="请输入歌手名"></label><input type="submit" value="上传">
</form>
</body>
</html>

5. 播放音乐

5.1前后端交互约定

在这里插入图片描述

5.2定义后端交互接口

@GetMapping("/get")
public ResponseEntity<byte[]> getMusic(@RequestParam String path){//如果客户端传来的path为空,或者字符串长度为0,那么就是一个异常参数,那么就是一个有问题的请求if(StringUtils.isAnyBlank(path)){return ResponseEntity.badRequest().build();}//返回字节类型的文件数据return musicService.getMusic(path);
}

5.3 在MusicService中定义描述播放音乐的接口

ResponseEntity<byte[]> getMusic(String path);

5.4在service包下的impl子包中实现接口中的getMusic抽象方法

我们使用了Files.reasAllBytes(String path):读取文件中的所有字节,读入内存,参数path是文件的路径(绝对路径)

@Override
public ResponseEntity<byte[]> getMusic(String path) {File file = new File(SAVE_PATH + path);byte[] a = null;try {//转化成为字节码数据a = Files.readAllBytes(file.toPath());//如果a为nullif(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为nullif(a == null){return ResponseEntity.badRequest().build();}

5.7 使用postman进行测试

在这里插入图片描述

5.8前端代码

<div style="width: 180px; height: 140px; position:absolute; bottom:10px; right:10px"><script type="text/javascript" src="player/sewise.player.min.js"></script><script type="text/javascript">SewisePlayer.setup({server: "vod",type: "mp3",//这里是默认的一个网址videourl: "http://jackzhang1204.github.io/materials/where_did_time_go.mp3",skin: "vodWhite",//这里需要设置falseautostart: "false",});</script>
</div>
//播放音乐
function playerSong(result) {console.log(result);//得到音乐的名字let musicName = result.substring(result.lastIndexOf("=") + 1);console.log(musicName);//retult 表示的是 url  musicName 表示的是音乐名称  0 表示的是音乐从什么时候开始播放 false表示的是是否启动自动播放SewisePlayer.toPlay(result, musicName, 0, true);
}

6. 删除音乐(单个删除,批量删除)

6.1 单个删除

6.1.1 前后端交互约定

在这里插入图片描述

6.1.2 删除业务实现逻辑描述

根据前端传来的音乐id 知道了是哪一首歌曲,如果根据这个id 在数据库中没有找到音乐文件,那么就提醒用户删除文件不存在,如果找到了该文件,那么就把数据库和服务器中的该音乐文件都给删了

6.1.3 在MusicMapper中声明删除单个音乐的接口和根据id查找音乐的接口
/*** 根据id 查找音乐*/
Music findMusicById(int id);
/*** 删除单个音乐*/
int deleteMusicById(int id);
6.1.4 在mybatis包中添加操作数据的SQL语句

添加根据id删除音乐文件和根据id查询音乐的SQL

<select id="findMusicById" resultType="com.example.onlinemusic.model.Music">select * from music where id = #{id}
</select><delete id="deleteMusicById" parameterType="java.lang.Integer">delete from music where id = #{id}</delete>
6.1.5后端实现接口
@PostMapping("/delete")
public ResponseBodyMessage<Boolean> deleteMusic(int id){if(id < 0){return new ResponseBodyMessage<>("参数错误",-1,false);}int i = musicService.deleteMusicById(id);if(i == -1){return new ResponseBodyMessage<>("该音乐不存在",-1,false);}else if(i == -2){return new ResponseBodyMessage<>("删除失败",-2,false);}return new ResponseBodyMessage<>("删除成功",1,true);
}
6.1.6 在业务层中创建MusicService接口中的deleteMusic抽象方法
int deleteMusicById(int musicId);
6.1.7 在业务层中service包中的impl子包中实现deleteMusic抽象方法(业务框架)
  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;}
    
@Overridepublic int deleteMusicById(int id) {Music music = musicMapper.findMusicById(id);if(music == null){return -1;}//删除数据库中的音乐int delete = musicMapper.deleteMusicById(id);if(delete != 1){return -2;}//删除服务器中的音乐//得到文件名int index = music.getUrl().lastIndexOf("=");String filename = music.getUrl().substring(index + 1);String path = SAVE_PATH + filename + ".mp3";File file = new File(path);System.out.println(file.toPath());//服务器删除失败if(!file.delete()){log.info("服务器删除失败");return -2;}return 1;}
6.1.8 使用postman进行测试

在这里插入图片描述

6.1.9前端代码
//删除音乐
function deleteSong(id) {$.ajax({url: '/music/delete',type: 'post',data: {"id": id},dataType: 'json',success: function (result) {if (result) {alert("删除成功,重新加载页面");window.location.assign("list.html");} else {alert("删除失败");}}})
}

6.2 实现批量删除

6.2.1 前后端交互约定

在这里插入图片描述

6.2.2 实现后端交互接口(是否批量删除成功)
@PostMapping("/deletepart")
public ResponseBodyMessage<Boolean> deletePartMusic(@RequestParam("id[]") List<Integer> id){//判断id长度是否为空if(id.size() == 0){return new ResponseBodyMessage<>("请选择歌曲",-1,false);}if(!musicService.deletePart(id)){return new ResponseBodyMessage<>("批量删除失败",-1,false);}return new ResponseBodyMessage<>("批量删除成功",1,true);
}
6.2.3 批量删除音乐逻辑描述

前端传来一个List,在这个List中每个元素都代表的是对应音乐文件的id,删除List中所有Id对应的音乐文件。但是还是一样的删除服务器中的文件的时候,要注意!!!

6.2.4业务层中创建MusicService接口中的deletePartMusic抽象方法
Boolean deletePart(List<Integer> id);
6.2.5在业务层中service包中的impl子包中实现deletePartMusic抽象方法(业务框架)

在数据库和服务器都把这个音乐给删了,我们就记位一次。

@Override
public Boolean deletePart(List<Integer> id) {int sum = 0;for(int i = 0;i < id.size();i++) {int musicIndex = id.get(i);Music music = musicMapper.findMusicById(musicIndex);if (music == null) {return false;}loveMusicMapper.deleteMusicById(music.getId());//删除数据库中的文件信息int ret = musicMapper.deleteMusicById(musicIndex);//删除服务器中的文件int index = music.getUrl().lastIndexOf("=");String filename = music.getUrl().substring(index + 1);String path = SAVE_PATH + filename + ".mp3";File file = new File(path);System.out.println(file.toPath());if (file.delete()) {sum += ret;}else {return false;}}return sum == id.size();
}
6.2.6使用postman进行测试

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.2.7 前端代码
//当页面加载完后 批量删除
$.when(load).done(function () {$("#delete").click(function () {let id = new Array();let i = 0;//遍历被选中$("input:checkbox").each(function () {if ($(this).is(":checked")) {id[i] = $(this).attr("id");i++;}})$.ajax({url: '/music/deletepart',type: 'post',data: {'id': id},dataType: 'json',success: function (result) {if (result) {alert("批量删除成功");window.location.assign("list.html");} else {alert("批量删除失败");}}})})

7. 查询音乐(支持模糊匹配)

7.1 前后端交互约定

在这里插入图片描述

在这里插入图片描述

7.2在MusicMapper中声明查询音乐的接口

 /*** 查询所有的音乐*/List<Music> findAllMusic();/*** 模糊匹配歌曲名*/List<Music> findMusicByFuzzyAndTitle(String title);

7.3 在mybatis包中添加操作数据的SQL语句

<select id="findAllMusic" resultType="com.example.onlinemusic.model.Music">select * from music;
</select>
<select id="findMusicByFuzzyAndTitle" resultType="com.example.onlinemusic.model.Music">select * from music where  title like concat('%',#{title},'%')
</select>

7.4 后端实现接口

@GetMapping("/findmusic")
public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String title){List<Music> musicList = musicService.findMusic(title);if(musicList == null){return new ResponseBodyMessage<>("查询列表为空",-1,null);}return new ResponseBodyMessage<>("查询成功",1,musicList);
}

7.5 在业务层中service包中的impl子包中实现findMusic抽象方法(业务框架)

@Override
public List<Music> findMusic(String title) {if(title == null){return musicMapper.findAllMusic();}return musicMapper.findMusicByFuzzyAndTitle(title);
}

7.6 使用postman进行测试

在这里插入图片描述
在这里插入图片描述

前端代码:

function load(title) {$.ajax({//从服务器上得到数据url: '/music/findmusic',type: 'get',dataType: 'json',data: {"title": title},success: function (result) {if (result == null) {alert("没有查询到这首歌");return;}console.log(result);//在这里result是一个数组,在数组中的每个元素中包含每个歌曲的 id singer urllet data = result.data;let s = '';for (let i = 0; i < data.length; i++) {let musicUrl = data[i].url+".mp3";s += '<tr>';s += '<th> <input id="'+data[i].id+'"type="checkbox"> </th>';s += '<td>' + data[i].title + '</td>';s += '<td>' + data[i].singer + '</td>';s+='<td > <button class="btn btn-primary" οnclick="playerSong(\''+musicUrl+'\')" >播放歌曲</button>' +'</td>';s+='<td > <button class="btn btn-primary" οnclick="deleteSong('+ data[i].id + ')" >删除</button>' +'<button class="btn btn-primary" οnclick="loveSong('+ data[i].id + ')" > 喜欢</button>'+'</td>';s += '</tr>';}$("#list").html(s);//把拼接好的页面添加到info的id下}})
}

8. 添加收藏音乐

8.1前后端交互约定

在这里插入图片描述

8.2 在model层下添加loveMuisic实体类

在实体类中包括 id 收藏音乐的Id,music_id 收藏音乐对应的在music表下到的Id,还有就是user_id 表示的是那个用户收藏的在user表中的id

package com.example.onlinemusic.model;import lombok.Data;@Data
public class LoveMusic {public int id;public int music_id;public int user_id;
}

8.3 添加收藏音乐的具体逻辑实现:

根据传来的musicId和从此时的登录态中得到的userId,在数据库中的lovermusic表中查看这个音乐是否存在,如果存在就提示用户此时无需添加,已经收藏。如果lovmusic表中不存在这个文件,那么就把这个音乐添加到lovmusic中,其实就是把这个muiscId添加到了music表中。

8.4 在LoveMusicMapper中声明添加收藏音乐的接口

    Music findLoveMusicByUserIdAndMusicId(int userId,int musicId);int insertMusic(int userId, int musicId);

8.5 在mybatis包中添加操作数据的SQL语句

<select id="findLoveMusicByUserIdAndMusicId" resultType="com.example.onlinemusic.model.Music">select * from lovemusic where music_id = #{musicId} and user_id = #{userId}
</select>
<insert id="insertMusic">insert into lovemusic values(null,#{userId},#{musicId})
</insert>

8.6 实现后端交互接口

@Autowired
private LoveMusicServiceImpl loveMusicService;
@PostMapping("/insert")
public ResponseBodyMessage<Boolean> insertMusic(int musicId, HttpServletRequest request){if(musicId < 0){return new ResponseBodyMessage<>("参数错误",-1,false);}int insert = loveMusicService.insertMusic(musicId, request);if(insert == -1){return new ResponseBodyMessage<>("该歌曲已收藏",-1,false);}else if(insert == -2){return new ResponseBodyMessage<>("收藏失败",-2,false);}return new ResponseBodyMessage<>("添加成功",1,true);
}

8.7 在业务层中创建LoveMusicService接口中的insertMusic抽象方法

 int insertMusic(int musicId, HttpServletRequest request);

8.8 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)

@Override
public int insertMusic(int musicId, HttpServletRequest request) {//得到当前的登录态User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);int userId = user.getId();//查询音乐是否已存在Music music= loveMusicMapper.findLoveMusicByUserIdAndMusicId(userId, musicId);if(music != null){return -1;}int ret = loveMusicMapper.insertMusic(userId, musicId);if(ret != 1){return -2;}return 1;
}

8.9 使用postman 进行测试

在这里插入图片描述

在这里插入图片描述

8.10 前端代码

function loveSong(musicId) {console.log(musicId);$.ajax({url: '/lovemusic/insert',type: 'post',data: {"musicId": musicId},dataType: 'json',success: function (result) {if (result.status === -1) {alert("该歌曲已收藏!!!");} else if (result.status === -2) {alert("收藏失败!!!")} else if(result.status === 0 ){alert("参数错误!!!")}else{alert("收藏成功");window.location.assign("list.html");}}})
}

9. 删除收藏音乐

9.1 前后端交互约定

在这里插入图片描述

9.2 在LoveMusicMapper中声明删除收藏音乐的接口

int deleteMusic(int userId,int musicId);

9.3 在mybatis包中添加操作数据的SQL语句

<delete id="deleteMusic">delete from lovemusic where user_id = #{userId} and music_id = #{musicId}
</delete>

9.4 实现后端交互接口

@PostMapping("/deletemusic")
public ResponseBodyMessage<Boolean> deleteMusic(int musicId,HttpServletRequest request){if(musicId < 0){return new ResponseBodyMessage<>("参数错误",-1,false);}int i = loveMusicService.deleteMusic(musicId,request);if(i == -1){return new ResponseBodyMessage<>("该音乐不存在",-1,false);}else if(i == -2){return new ResponseBodyMessage<>("删除失败",-2,false);}return new ResponseBodyMessage<>("删除成功",1,true);
}

9.5在业务层中创建LoveMusicService接口中的insertMusic抽象方法

int deleteMusic(int music,HttpServletRequest request);

9.6 在业务层中service包中的impl子包中实现insertMusic抽象方法(业务框架)

@Override
public int deleteMusic(int musicId, HttpServletRequest request) {User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);Music music = loveMusicMapper.findLoveMusicByUserIdAndMusicId(user.getId(),musicId);if(music == null){return -1;}//删除数据库中的音乐int delete = loveMusicMapper.deleteMusic(user.getId(),musicId);if(delete != 1){return -2;}return 1;
}

9.7 使用postman 进行测试

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

9.8 前端代码

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

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

10.1 前后端交互约定

在这里插入图片描述

@GetMapping("/findmusic")
public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String title,HttpServletRequest request){User user = (User) request.getSession().getAttribute(USERINFO_SESSION_KEY);List<Music> musicList = loveMusicService.findMusic(title,user.getId());if(musicList == null){return new ResponseBodyMessage<>("查询列表为空",-1,null);}return new ResponseBodyMessage<>("查询成功",1,musicList);
}

10.2在LoveMusicMapper中声明删除收藏音乐的接口

//查看user喜欢的所有音乐
List<Music> findAllMusic(int userId);
//查看user输入的title 也就是歌曲名相关的有关音乐
List<Music> findMusicByFuzzyAndTitle(String title,int userId);

10.3在mybatis包中添加操作数据的SQL语句


<resultMap id="BaseMap1" type="com.example.onlinemusic.model.Music"><id column="id" property="id" /><id column="title" property="title" /><id column="singer" property="singer" /><id column="time" property="time" /><id column="url" property="url" /><id column="userid" property="userId" />
</resultMap>
<select id="findAllMusic" resultMap="BaseMap1">select m.* from lovemusic lm,music m where m.id = lm.music_id and lm.user_id=#{userId}
</select><resultMap id="BaseMap2" type="com.example.onlinemusic.model.Music"><id column="id" property="id" /><id column="title" property="title" /><id column="singer" property="singer" /><id column="time" property="time" /><id column="url" property="url" /><id column="userid" property="userId" />
</resultMap>
<select id="findMusicByFuzzyAndTitle" resultMap="BaseMap2">select m.* from lovemusic lm,music m where m.id = lm.music_id and lm.user_id=#{userId} and title like concat('%',#{title},'%')
</select>

10.4在业务层中创建LoveMusicService接口中的findMusic抽象方法


List<Music> findMusic(String title,int userId);

10.5 在业务层中service包中的impl子包中实现findMusic抽象方法(业务框架)

@Override
public List<Music> findMusic(String title,int userId) {if(title == null){return loveMusicMapper.findAllMusic(userId);}return loveMusicMapper.findMusicByFuzzyAndTitle(title,userId);
}

10.5 使用postman进行测试

在这里插入图片描述

11.代码完善

其实我们在删除的时候应当注意,我们的music表中的数据应该和lovemusic表中的数据保持一致,也就是如果我现在music页面和lovemusic页面有有同一首歌,如果把music页面中的这一首歌给删了,那么我们的这个lovmusic表中的这个音乐的数据,那么也就没了。但是反过来是不一样的,把lovemusic表中的数据,无论我们怎么删。都是和music表是无关的。

只能是music ------> lovemusic

使用musicId删除lovemusic表中的数据

loveMusicMapper.deleteMusicById(music.getId());

修改后的deleteMusicById()

@Override
public int deleteMusicById(int id) {Music music = musicMapper.findMusicById(id);if(music == null){return -1;}//删除数据库中的音乐int delete = musicMapper.deleteMusicById(id);if(delete != 1){return -2;}//删除和这个音乐相关的在lovemusic中的歌曲loveMusicMapper.deleteMusicById(music.getId());//删除服务器中的音乐//得到文件名int index = music.getUrl().lastIndexOf("=");String filename = music.getUrl().substring(index + 1);String path = SAVE_PATH + filename + ".mp3";File file = new File(path);System.out.println(file.toPath());if (!file.delete()) {return -2;}return 1;
}

修改后的deletePart()

@Override
public Boolean deletePart(List<Integer> id) {int sum = 0;for(int i = 0;i < id.size();i++) {int musicIndex = id.get(i);Music music = musicMapper.findMusicById(musicIndex);if (music == null) {return false;}//删除数据库中的文件信息int ret = musicMapper.deleteMusicById(musicIndex);//删除服务器中的文件int index = music.getUrl().lastIndexOf("=");String filename = music.getUrl().substring(index + 1);String path = SAVE_PATH + filename + ".mp3";File file = new File(path);System.out.println(file.toPath());if (file.delete()) {//删除和这个音乐相关的在lovemusic中的歌曲loveMusicMapper.deleteMusicById(music.getId());sum += ret;}else {return false;}}return sum == id.size();
}

11.设置登录拦截器

首先在这里声明一点,我们此时虽然已经完成了,项目的大部分逻辑,但是如果我现在直接使用访问用户收藏音乐界面是可以访问到的。因为我们还没有设置拦截器。

创建一个用于配置拦截器的包—webConfig包,在这个包中在声明一个用户的登录拦截器。

@Configuration
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic 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 {@Overridepublic 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=UTCpassword: 123456username: rootdriver-class-name: com.mysql.cj.jdbc.Driverservlet:multipart.max-file-size: 15MBmultipart.max-request-size: 100MB# 开启 MyBatis SQL 打印
logging:level:root: infodruid:sql:Statement: debugcom: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,项目也是执行的。

相关文章:

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

文章目录1. 项目描述:2. 项目上线展现&#xff1a;3. 项目具体实现&#xff1a;1. 登录2. 注册3.退出系统4.添加音乐4.1前后端交互约定4.2上传文件业务逻辑&#xff1a;4.3创建model包中的music类4.4在MusicMapper接口中&#xff0c;声明insertMusic抽象方法4.5在mybatis包中添…...

Spring Cloud Alibaba--gateway微服务详解之网关(二)

1、网关介绍 上篇对微服务中的nacos注册中心进行集成讲解。nacos主要作用是管理多服务之间复杂关系的组件。微服务是非常庞大且问题突出的架构&#xff0c;HTTP协议具有跨源资源共享 (CORS) Cross- Origin Resource Sharing机制&#xff0c;而处于安全考虑往往前端架构都会对跨…...

Zynq非VDMA方案实现视频3帧缓存输出,无需SDK配置,提供工程源码和技术支持

目录1、前言2、VDMA的不便之处3、FDMA取代VDMA实现视频缓存输出4、Vivado工程详解5、上板调试验证并演示6、福利&#xff1a;工程代码的获取1、前言 对于Zynq和Microblaze的用户而言&#xff0c;要想实现图像缓存输出&#xff0c;多半要使用Xilinx推荐的VDMA方案&#xff0c;该…...

血液透析过滤芯气密性检测装置中的高精度多段压力控制解决方案

摘要&#xff1a;针对目前血液过滤芯气密性检测过程中存在的自动化水平较低、多个检测压力之间需人工切换和压力控制精度较差的问题&#xff0c;为满足客户对高精度和自动化气密性检测的要求&#xff0c;本文提出了相应的解决方案。解决方案的主要特点是全过程的可编程压力控制…...

PDF加密如何批量解除?快来了解下这个方法

在现代办公环境中&#xff0c;PDF文档的使用非常普遍。然而&#xff0c;由于一些安全需求&#xff0c;有时候PDF文档会被加密&#xff0c;使得只有授权人员可以查看或修改它。但是&#xff0c;如果您需要对许多加密PDF文档进行操作&#xff0c;逐个解密这些文档可能非常费时费力…...

C++——哈希4|布隆过滤器

目录 布隆过滤器 完整代码 布隆过滤器应用 布隆过滤器的查找 布隆过滤器删除 布隆过滤器优点 布隆过滤器缺陷 布隆过滤器海量数据处理 布隆过滤器 位图只能映射整形&#xff0c;而对于字符串却无能为力。 把字符串用哈希算法转成整形&#xff0c;映射一个位置进行标…...

python冒号的用法总结

一维数组 1. 单个冒号的情况 1.1 写完整的情况下 单个冒号的情况下&#xff0c;对数组的遍历操作是从前向后操作。如&#xff1a;arr[a:b] &#xff0c;冒号前的a含义是从a开始遍历&#xff0c;冒号后的b含义是到b截止&#xff08;不包括b&#xff09;。 arr [1, 2, 3, 4,…...

面试题整理

面试题整理 一、Java基础 1、Java 语言有哪些特点 简单易学&#xff1b; 面向对象&#xff08;封装&#xff0c;继承&#xff0c;多态&#xff09;&#xff1b; 平台无关性&#xff08; Java 虚拟机实现平台无关性&#xff09;&#xff1b; 支持多线程&#xff08; C 语言…...

C语言深度解剖-关键字(7)

目录 switch case 语句 理解&#xff1a; 补充&#xff1a; 深入理解&#xff1a; default 语句&#xff1a; case语句&#xff1a; 总结&#xff1a; do、while、for 关键字 while for do while 各种死循环方法&#xff1a; while for do while getchar 写在…...

利用JavaScript编写Python内置函数查询工具

最近我开始学习Python编程语言&#xff0c;我发现Python拥有非常丰富的内置函数&#xff0c;可以用来实现各种不同的功能。但是每当我需要查找一个内置函数时&#xff0c;我总是需要联网使用搜索引擎进行查询。这种方式不仅费时费力&#xff0c;而且需要联网&#xff0c;很不方…...

【MySQL进阶】SQL优化

&#x1f60a;&#x1f60a;作者简介&#x1f60a;&#x1f60a; &#xff1a; 大家好&#xff0c;我是南瓜籽&#xff0c;一个在校大二学生&#xff0c;我将会持续分享Java相关知识。 &#x1f389;&#x1f389;个人主页&#x1f389;&#x1f389; &#xff1a; 南瓜籽的主页…...

最新版海豚调度dolphinscheduler-3.1.3配置windows本地开发环境

0 说明 本文基于最新版海豚调度dolphinscheduler-3.1.3配置windows本地开发环境&#xff0c;并在windows本地进行调试和开发 1 准备 1.1 安装mysql 可以指定为windows本地mysql&#xff0c;也可以指定为其他环境mysql&#xff0c;若指定为其他环境mysql则可跳过此步。 我这…...

csv文件完整操作总结

csv文件完整操作总结 1.概述 csv 模块主要用于处理从电子数据表格Excel或数据库中导入到文本文件的数据&#xff0c;通常简称为 comma-separated value &#xff08;CSV&#xff09;格式因为逗号用于分离每条记录的各个字段。 2.读写操作 2.1.测试数据 创建一个test.csv文…...

时间序列预测--基于CNN的股价预测(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 时间序列预测有很多方法&#xff0c;如传统的时序建模方法ARIMA、周期因子法、深度学习网络等&#xff0c;本次实验采用最简单的…...

Dubbo与Spring Cloud优缺点分析(文档学习个人理解)

文章目录核心部件1、总体框架1.1 Dubbo 核心部件如下1.2 Spring Cloud 总体架构2、微服务架构核心要素3、通讯协议3.1 Dubbo3.2 Spring Cloud3.3 性能比较4、服务依赖方式4.1 Dubbo4.2 Spring Cloud5、组件运行流程5.1 Dubbo5.2 Dubbo 运行组件5.3 Spring Cloud5.4 Spring Clou…...

单元测试工具——JUnit的使用

⭐️前言⭐️ 本篇文章主要介绍单元测试工具JUnit的使用。 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主将持续更新学习记录收获&#xff0c;友友们有任何问题可以在评论区留言 &#x1f349;博客中涉及源码…...

Linux_基本权限

Linux入门第二篇已送达&#xff01; Linux_基本权限shell外壳权限Linux的用户分类角色划分Linux的文件文件类型查看权限目录的权限默认权限粘滞位shell外壳 为了保护操作系统&#xff0c;用户的指令不能由操作系统直接进行执行&#xff0c;需要一个中间者&#xff0c;比如Linu…...

3、JavaScript面试题

1, Js数据类型有哪些&#xff1f;数值、字符串、布尔、undefined、null、数组、对象、函数2, 引用类型和值类型的区别- 值类型存在于栈中, 存取速度快 引用类型存在于堆,存取速度慢- 值类型复制的是值本身 引用类型复制的是指向对象的指针- 值类型结构简单只包含基本数据, 引用…...

YUV图像

YUV的存储方式UV格式有两大类&#xff1a;planar和packed。对于planar的YUV格式&#xff0c;先连续存储所有像素点的Y&#xff0c;紧接着存储所有像素点的U&#xff0c;随后是所有像素点的V。对于packed的YUV格式&#xff0c;每个像素点的Y,U,V是连续交替存储的。YUV的采样主流…...

.net6API使用AutoMapper和DTO

AutoMapper&#xff0c;是一个转换工具&#xff0c;说到AutoMapper时&#xff0c;就不得不先说DTO&#xff0c;它叫做数据传输对象(Data Transfer Object)。 通俗的来说&#xff0c;DTO就是前端界面需要用的数据结构和类型&#xff0c;而我们经常使用的数据实体&#xff0c;是数…...

IO知识整理

IO 面向系统IO page cache 程序虚拟内存到物理内存的转换依靠cpu中的mmu映射 物理内存以page&#xff08;4k&#xff09;为单位做分配 多个程序访问磁盘上同一个文件&#xff0c;步骤 kernel将文件内容加载到pagecache多个程序读取同一份文件指向的同一个pagecache多个程…...

【正点原子FPGA连载】第十三章QSPI Flash读写测试实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第十三章QSPI Fl…...

深入理解mysql的内核查询成本计算

MySql系列整体栏目 内容链接地址【一】深入理解mysql索引本质https://blog.csdn.net/zhenghuishengq/article/details/121027025【二】深入理解mysql索引优化以及explain关键字https://blog.csdn.net/zhenghuishengq/article/details/124552080【三】深入理解mysql的索引分类&a…...

LeetCode 141. 环形链表

原题链接 难度&#xff1a;easy\color{Green}{easy}easy 题目描述 给你一个链表的头节点 headheadhead &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 nextnextnext 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的…...

git提交

文章目录关于数据库&#xff1a;桌面/vue-admin/vue_shop_api 的 git 输入 打开 phpStudy ->mySQL管理器 导入文件同时输入密码&#xff0c;和文件名 node app.js 错误区&#xff1a; $ git branch // git branch 查看分支 只有一个main分支不见master解决&#xff1a; gi…...

Java中常见的编码集问题

收录于热门专栏Java基础教程系列&#xff08;进阶篇&#xff09; 一、遇到一个问题 1、读取CSV文件 package com.guor.demo.charset;import java.io.BufferedReader; import java.io.FileReader; import java.util.ArrayList; import java.util.HashMap; import java.util.L…...

数据结构与算法(Java版) | 就让我们来看看几个实际编程中遇到的问题吧!

上一讲&#xff0c;我给大家简单介绍了一下数据结构&#xff0c;以及数据结构与算法之间的关系&#xff0c;照理来说&#xff0c;接下来我就应该要给大家详细介绍线性结构和非线性结构了&#xff0c;但是在此之前&#xff0c;我决定还是先带着大家看几个实际编程中遇到的问题&a…...

【C++算法】dfs深度优先搜索(上) ——【全面深度剖析+经典例题展示】

&#x1f483;&#x1f3fc; 本人简介&#xff1a;男 &#x1f476;&#x1f3fc; 年龄&#xff1a;18 &#x1f4d5; ps:七八天没更新了欸&#xff0c;这几天刚搞完元宇宙&#xff0c;上午一直练&#x1f697;&#xff0c;下午背四级单词和刷题来着&#xff0c;还在忙一些学弟…...

总结高频率Vue面试题

目录 什么是三次握手&#xff1f; 什么是四次挥手&#xff1f;&#xff08;close触发&#xff09; 什么是VUEX&#xff1f; 什么是同源----跨域&#xff1f; 什么是Promise&#xff1f; 什么是fexl布局&#xff1f; 数据类型 什么是深浅拷贝&#xff1f; 什么是懒加载&…...

IP协议详解

目录 前言&#xff1a; IP协议 提出问题 解决方案 地址管理 子网掩码 路由选择 小结&#xff1a; 前言&#xff1a; IP协议作为网络层知名协议。当数据经过传输层使用TCP或者UDP对数据进行封装&#xff0c;然后当数据到达网络层&#xff0c;基于TCP或UDP数据包继续进行…...

自己做网站怎么选架构/威海seo优化公司

原标题&#xff1a;一起来捉妖&#xff1a;在线6小时被劝退&#xff1f;只需网络断开&#xff0c;跳过等待15分钟一起来捉妖这款游戏刚出来的时候就凭借着新颖的玩法吸引了大部分玩家的驻足&#xff0c;而且这个游戏也是让玩家们完全停不下手&#xff0c;不过之卿由于已经成年所…...

一个网站如何优化/seo专员工作容易学吗

一、题目 二、思路 审题nums[i]都在int范围内&#xff08;32位二进制&#xff09;&#xff0c;对于每个num[i]的二进制数&#xff0c;对于第j个位置的元素都相加&#xff0c;并且最后对结果的二进制数&#xff0c;其第j个位置的元素依次进行余3操作。关键&#xff1a;对于数组…...

精品在线开发网站建设/国内真正的永久免费建站

一、项目需求 最近接了一个需求&#xff0c;用户手动在画板上进行绘制图案&#xff0c;绘制完成后&#xff0c;将绘制好的作品上传到服务器&#xff0c;涉及到的功能点&#xff08;这篇文章有点长&#xff09;&#xff1a; 项目的运行环境是在手机端&#xff0c;canvas 的尺寸…...

济南网站制作推广/百度seo查询

苏生不惑第249篇原创文章&#xff0c;将本公众号设为星标&#xff0c;第一时间看最新文章。之前分享过如何有效的维权&#xff0c;这些投诉平台你可能用得上 &#xff0c;今天分享几个实用的政府网站&#xff0c;先收藏起来&#xff0c;万一哪天能用上。中国法律服务网一个终极…...

中小网站建设都有哪些方案/网络营销学校

学习整理&#xff0c;部分描述来源于网络 1. 概述 1.1 定义 k近邻法&#xff08;k-nearest neighbor&#xff0c;k-NN&#xff09;分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻&#xff0c;就是k个最近的邻居的意思&#xff0c;说的是每个样本都可以用它最接近…...

大连b2c网站建设/济南最新消息

一个简单的跨平台的按钮组件。可以进行一些简单的定制。 如果这个组件外观并不怎么搭配你的设计&#xff0c;那你可以使用TouchableOpacity或是TouchableNativeFeedback组件来制作自己所需要的按钮&#xff0c;视频教程如何制作一个按钮讲述了完整的过程。或者你也可以在github…...