【 在线音乐平台(onlinemusic) 】

news2024/9/20 18:47:29

文章目录

  • 一、核心功能
  • 二、效果演示
  • 三、创建项目
  • 四、数据库设计及配置数据库
    • 4.1 数据库和表设计
    • 4.2 配置连接数据库
  • 五、创建配置类
  • 六、具体功能实现
    • 6.1 注册模块
    • 6.2 登录模块
      • 拓展:登录注册加密(MD5,BCrypt)
    • 6.3 退出模块
    • 6.4 上传音乐模块
        • 知识拓展1:如何判断上传的文件是mp3
        • 知识拓展2:时间格式化的类:SimpleDateFormat
    • 6.5 播放音乐模块
      • 难点:添加前端开源播放器控件
    • 6.6 删除音乐模块
      • 6.6.1 删除单个音乐
      • 6.6.2 批量删除选中音乐
    • 6.7 查询音乐模块
    • 6.8 添加音乐至喜欢的列表模块
    • 6.9 查询收藏的音乐模块
    • 6.10 删除收藏的音乐模块
  • 七、配置拦截器
  • 八、测试用例(了解即可)
  • 九、项目部署
  • 十、项目拓展方向

一、核心功能

  1. 登录,注册,退出(加密操作)
  2. 上传,播放音乐
  3. 删除指定音乐
  4. 批量删除选中的音乐
  5. 查询你想要的音乐(支持模糊查询和全查询)
  6. 添加音乐至喜欢的列表
  7. 移除喜欢的音乐

在这里插入图片描述


二、效果演示

  1. 登录界面
    在这里插入图片描述

  2. 注册界面
    在这里插入图片描述

  3. 主页面
    在这里插入图片描述

  4. 收藏页面
    在这里插入图片描述

细节演示,后续说明 !!


三、创建项目

创建一个springboot项目,具体步骤与前面的博客记录一样,这里就不再重复赘述,大家自行参考以下博客:

1.springboot项目的基本创建

2.添加mybatis框架支持

四、数据库设计及配置数据库

4.1 数据库和表设计

思维导图如下:

在这里插入图片描述

具体创建:

  1. 创建数据库musicserver
-- 数据库
drop database if exists `onlinemusic`;
create database if not exists `musicserver` character set utf8;
-- 使用数据库
use `onlinemusic`;
  1. 创建表user
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`password` varchar(255) NOT NULL
);
  1. 创建表music
DROP TABLE IF EXISTS `music`;
CREATE TABLE `music` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`title` varchar(50) NOT NULL,
`singer` varchar(30) NOT NULL,
`time` varchar(13) NOT NULL,
`url` varchar(1000) NOT NULL,
`userid` int(11) NOT NULL
);

title字段为歌曲名称,url字段为歌曲的路径

  1. 创建中间表lovemusic
DROP TABLE IF EXISTS `lovemusic`;
CREATE TABLE `lovemusic` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`music_id` int(11) NOT NULL
)

4.2 配置连接数据库

打开application.properties配置如下信息:

#配置数据库
#spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&useSSL=false
#spring.datasource.username=root
#spring.datasource.password=123456
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 上传服务器后,此处需要修改如下:外网IP ,服务器上的数据库没有密码
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# 音乐上传路径  不推荐使用中文路径  且分隔符不能采用 \\ 因为 Linux中不支持
#music.local.path=F:/music/
# 上传服务器后,此处需要修改为服务器中的地址  注意最后一个 /
music.local.path=/root/savaonlinemusic/

#配置xml
mybatis.mapper-locations=classpath:mybatis/**Mapper.xml

# 配置springboot日志调试模式是否开启
debug=true

# 设置打印日志的级别,及打印sql语句
#日志级别:trace,debug,info,warn,error
#基本日志
logging.level.root=INFO
logging.level.com.example.onlinemusic.mapper=debug


#扫描的包:druid.sql.Statement类和frank包
logging.level.druid.sql.Statement=DEBUG
logging.level.com.example=DEBUG

#配置springboot上传文件的大小,默认每个文件的配置最大为15Mb,单次请求的文件的总数不能大于100Mb
spring.servlet.multipart.max-file-size = 15MB
spring.servlet.multipart.max-request-size=100MB



五、创建配置类

创建 tools 工具包,放置全局可以使用的配置类

  1. 设置统一返回格式
package com.example.onlinemusic.tools;

import lombok.Data;

@Data
public class ResponseBodyMessage<T> {
    private int status;//状态码
    private String message;//状态描述信息[出错的原因? 没出错的原因?]
    private T data;//返回的数据

    public ResponseBodyMessage(int status, String message, T data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

注意: 将返回类 ResponseBodyMessage 设置为泛型,方便返回数据类型的设置

  1. Constant类,储存复杂的不变常量

如设置了session对象,此时的key值是一个字符串,将来在其他地方获取对应的session需要通过这个字符串获取,但是存在一定的写错的情况。所以,此时建议把他定义为一个常量

public class Constant {
    public static final String USERINFO_SESSION_KEY = "USERINFO_SESSION_KEY";
}

六、具体功能实现

6.1 注册模块

  1. Controller 层实现
    @Autowired
    private UserService userService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    //注册
    @RequestMapping("/reg")
    public ResponseBodyMessage<Boolean> reg(@RequestParam String username, @RequestParam String password) {
        User user1 = userService.selectByName(username);
        if (user1 != null) {
            return new ResponseBodyMessage<>(-1, "当前用户已存在!", false);
        } else {
            User user2 = new User();
            user2.setUsername(username);
            String password1 = bCryptPasswordEncoder.encode(password);
            user2.setPassword(password1);
            userService.addUser(user2);
            return new ResponseBodyMessage<>(1, "注册成功!", true);
        }
    }

注意: 用户存在就不能再次注册

  1. Mapper 层实现
public int addUser(User user);
  1. xml 文件实现
    <!-- 新增用户 -->
    <insert id="addUser">
        insert into user(username,password) values (#{username},#{password});
    </insert>
  1. 请求和响应设计

请求
POST /user/reg
{username: “”,password: “”}
响应
{
status: 1/-1,
message: “”,
data: “”
}

6.2 登录模块

  1. 创建User类
    在package com.example.musicserver.model包中创建User类
package com.example.onlinemusic.model;

import lombok.Data;

@Data
public class User {
    private int id;
    private String username;
    private String password;
}

  1. 创建对应的Mapper和Service

2.1新建mapper包,新建UserMapper

package com.example.onlinemusic.mapper;

import com.example.onlinemusic.model.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    public User login(User loginUser);
}

2.2创建UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.onlinemusic.mapper.UserMapper">
    <select id="login" resultType="com.example.onlinemusic.model.User">
        select * from user where username=#{username} and password=#{password}
    </select>
</mapper>

2.3创建Service类

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User login(User userlogin){
        return userMapper.login(userlogin);
    }
 }
  1. 实现登录

3.1登录的请求和响应设计

请求
POST /user/login
{username: “”,password: “”}
响应
{
status: 0/-1,
message: “”,
data: “”
}

响应体设计字段解释:
{
status:状态码,为1代表成功,负数代表失败
message:状态描述信息,描述此次请求成功或者失败的原因
data:返回的数据,请求成功后,需要给前端的数据信息
}

3.2创建UserController类

    @Autowired
    private UserService userService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    
    @RequestMapping("/login1")
    //将请求参数绑定到你控制器的方法参数上 。如果这个参数是非必传的可以写为:@RequestParam(required = false) ,默认是true
    public ResponseBodyMessage<Boolean> login1(@RequestParam String username, @RequestParam String password, HttpServletRequest request) {


        User user = userService.selectByName(username);

        if (user != null) {
            // bCryptPasswordEncoder 采用注入的方式
            boolean flg = bCryptPasswordEncoder.matches(password, user.getPassword());
            if (!flg) {
                return new ResponseBodyMessage<>(-1, "密码错误 !", false);
            } else {
                System.out.println("登录成功 !");
                // 登录成功,将用户信息写入Session中
                request.getSession().setAttribute(Constant.USERINFO_SESSION_KEY, user);
                return new ResponseBodyMessage<>(1, "登录成功 !", true);
            }

        } else {
            System.out.println("登录失败 !");
            return new ResponseBodyMessage<>(-1, "登录失败 !", false);
        }
    }

拓展:登录注册加密(MD5,BCrypt)

  1. MD5 加密
    MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能得到原始的明文,即其过程不可逆; 但是虽然不可逆,但是不是说就是安全的。因为自从出现彩虹表后,这样的密码也"不安全"。

彩虹表:彩虹表就是一个庞大的、针对各种可能的字母组合预先计算好的哈希值的集合,不一定是针对MD5算法的,各种算法的都有,有了它可以快速的破解各类密码。越是复杂的密码,需要的彩虹表就越大,现在主流的彩虹表都是100G以上。

不安全的原因:

  1. 暴力攻击速度很快
  2. 字典表很大
  3. 碰撞

更安全的做法是加盐或者长密码等做法,盐是在每个密码中加入一些单词来变成一个新的密码,存入数据库当中,让整个加密的字符串变的更长,破解时间变慢。密码学的应用安全,是建立在破解所要付出的成本远超出能得到的利益上的 。

但是,因为这里没有用随机盐值,固定盐值使得每次加密的密码都是固定的,也存在暴力破解风险。更安全的是,当密码长度很大,盐值也是随机的情况下,密码的强度也加大了。破解成本也增加了,也就是下面介绍的 BCrypt

(重点参考)MD5详解:https://md5.cc/news1.aspx

  1. BCrypt 加密
    Bcrypt就是一款加密工具,可以比较方便地实现数据的加密工作。你也可以简单理解为它内部自己实现了随机加盐处理 。我们使用MD5加密,每次加密后的密文其实都是一样的,这样就方便了MD5通过大数据的方式进行破解。Bcrypt生成的密文是60位的。而MD5的是32位的。Bcrypt破解难度更大。

在我们此项目中也是使用了 BCrypt 加密,具体操作如下

添加依赖:

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

在 SpringBoot 启动了添加:

@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})

解释: 因为在SpringBoot中,默认的Spring Security生效了的,此时的接口都是被保护的,我们需要通过验证才能正常的访问。此时通过上述配置,即可禁用默认的登录验证该项目本身没有那么复杂,并没有用到 Spring Security 框架,只是用到了其中的 BCryptPasswordEncoder 类

最后就是登录注册中分别调用的 encode 和 matches 方法:

encode方法:对用户密码进行加密
matches方法:参数一,待检验的未加密的密码 。参数二:从数据库中查询出的加密后密码


总结:

  1. 密码学的应用安全,是建立在破解所要付出的成本远超出能得到的利益上的 。
  2. 使用BCrypt相比于MD5加密更好的一点在于,破解的难度上加大
  3. BCrypt的破解成本增加了,导致系统的运行成本也会大大的增加 。
  4. 回到本质的问题,你的数据库中的数据价值如何?如果你是银行类型的,那么使用BCrypt是不错的,一般情况使用MD5加盐,已经够用了。

BCrypt加密: 一种加盐的单向Hash,不可逆的加密算法,同一种明文(plaintext),每次加密后的密文都不一样,而且不可反向破解生成明文,破解难度很大。
MD5加密: 是不加盐的单向Hash,不可逆的加密算法,同一个密码经过hash的时候生成的是同一个hash值,在大多数的情况下,有些经过md5加密的方法将会被破解。

Bcrypt生成的密文是60位的。而MD5的是32位的。

目前,MD5和BCrypt比较流行。相对来说,BCrypt比MD5更安全,但加密更慢。 虽然BCrpyt也是输入的字符串+盐,但是与MD5+盐的主要区别是:每次加的盐不同,导致每次生成的结果也不相同。无法比对!


6.3 退出模块

所谓退出即将Session中的用户信息删除掉,这样只有再次登录才能进行后续页面访问

  1. Controller 层实现
 @RequestMapping("/logout")
    public ResponseBodyMessage<Boolean> logout(HttpServletRequest request, HttpServletResponse response) {
        //退出登录则清除session中的用户信息
        HttpSession session = request.getSession(false);
        if (session == null) {
            System.out.println("您还未登录!");
            return new ResponseBodyMessage<>(-1, "您还未登录!", false);
        } else {

            session.removeAttribute(Constant.USERINFO_SESSION_KEY);

            if (session.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
                System.out.println("清除session中的用户信息成功!");
//                try {
//                    response.sendRedirect("login.html");//此处要注意,只需要将后端数据传给前端,让前端来判断是否需要重定向即可
//                } catch (IOException e) {               //若后端也进行重定向,就可能出错 !!
//                    e.printStackTrace();
//                }
                return new ResponseBodyMessage<>(1, "退出成功!", true);
            }
            return new ResponseBodyMessage<>(-1, "退出失败!", false);
        }
    }
}
  1. 前端实现
    <script>
        function exit() {
            // 2.发送 ajax 请求到后端
            jQuery.ajax({
                url: "/user/logout",
                type: "POST",
                dataTye: "json",
                success: function (data) {
                    if (data.status == 1) {
                        alert("退出登录成功!");
                        window.location.href = "login.html";
                    } else {
                        alert("退出失败,请重试!");
                    }
                }
            });
        }

        $(function (){
            $("#logout").click(function (){
                exit();
            });
        });
    </script>

6.4 上传音乐模块

  1. 请求响应设计

请求:
{
post,
/music/upload
{singer,MultipartFile file},
}
响应:
{
“status”: 1,
“message”: “上传成功!”,
“data”: true
}

  1. 新建Music类:
package com.example.onlinemusic.model;

import lombok.Data;

@Data
public class Music {
    private int id;
    private String title;
    private String singer;
    private String url;
    private String time;
    private int userid;
}

  1. 创建MusicController类
    上传本地成功后,还要实现数据库上传,需要先进行数据库的查询,然后插入数据库
@RestController
@RequestMapping("/music")
public class MusicController {

    @Value("${music.local.path}")
    private String Save_path;

    @Autowired
    private UserService userService;

    @RequestMapping("/upload")
    // MultipartFile是Spring框架中处理文件上传的主要类
    public ResponseBodyMessage<Boolean> insertMusic(@RequestParam String singer, @RequestParam("filename") MultipartFile file,
                                                    HttpServletRequest request, HttpServletResponse response) {
        // 没有获取到 Session 不会创建
        HttpSession httpSession = request.getSession(false);
        // 1.验证是否登录,登录成功后才允许上传
        if (httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
            System.out.println("没有登录!");
            return new ResponseBodyMessage<>(-1, "没有登陆,请登录后上传 !", false);
        } else {
            // 获取音乐文件名称及类型 如 xxx.mp3
            String filenameAndType = file.getOriginalFilename();
            System.out.println("filenameAndType--->>>>>>>>>>>>>>>>>" + filenameAndType);

            // 设置文件保存路径 Save_path 采取从配置文件中读取的方式
            String path = Save_path + filenameAndType;

            //2.上传服务器  上传音乐到指定的文件目录
            // 创建文件,该文件路径为 path
            File dest = new File(path);
            if (!dest.exists()) {
                dest.mkdirs();
            }
            try {
                // 将上传的文件保存在 dest 目录下
                file.transferTo(dest);
                //return new ResponseBodyMessage<>(1, "服务器上传音乐成功 !", true);
            } catch (IOException e) {
                e.printStackTrace();
                return new ResponseBodyMessage<>(-1, "服务器上传音乐失败 !", false);
            }

            //3.上传数据库
            //3.1 先准备好歌曲的字段数据
            int LastPointIndex = filenameAndType.lastIndexOf(".");//返回上传文件名称最后一个点的下标
            //数据库当中存储的歌曲名称不包含.mp3. 所以需要进行截取
            String title = filenameAndType.substring(0, LastPointIndex);// substring 截取的是左闭右开区间,我们只需要从0下标到最后一个点的下标

            //SimpleDateFormat来格式化当前的系统时间
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
            String time = simpleDateFormat.format(new Date());

            //这里的 url 会被用到播放音乐的模块
            String url = "/music/get?path=" + title;

            User user = (User) httpSession.getAttribute(Constant.USERINFO_SESSION_KEY);
            int userid = user.getId();

            // TODO:这里可以有一步:验证上传的文件在数据库中是否已经存在【判断title 和 singer 是否有和数据库中的数据一致的】
            // 此处遇到的问题:1.端口被进程占用,如何处理! 先在cmd中查看被那个进程占用,再用命令杀掉该进程
            //              2.mybatis xml文件中,查询语句的返回类型只能是对应的实体,不能是受影响的行数
            Music music = userService.selectinsertMusic(title, singer);

            if (music != null) {
                return new ResponseBodyMessage<>(-1, "该音乐已存在,上传数据库失败!", false);
            } else {
                //3.2 将数据插入数据库
                int ret = userService.insertMusic(title, singer, time, url, userid);

                if (ret == 1) {
                    //Ctrl+alt+T 快速生产 try catch 包裹
                    try {
                        // 当上传数据库成功后,会跳转到音乐列表界面
                        response.sendRedirect("/list.html");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    return new ResponseBodyMessage<>(1, "数据库上传音乐成功 !", true);
                } else {
                    // 如果上传数据库失败,就会把服务器中的文件删除掉 !
                    dest.delete();
                    return new ResponseBodyMessage<>(-1, "数据库上传音乐失败,已删除服务器上传的音乐!", false);
                }
            }
        }
    }
  1. 使用@Value(“${music.local.path}”),获取到配置文件当中的值。不建议中文路径。
  2. MultipartFile类,在org.springframework.web.multipart包当中,是Spring框架中处理文件上传的主要类
    主要方法介绍:
    在这里插入图片描述

MultipartFile详解:https://www.jianshu.com/p/e3d798c906cd


  1. 定义接口MusicMapper
@Mapper
public interface MusicMapper {
    //上传音乐
    public int insertMusic(String title, String singer, String time, String url, int userid);

    //上传音乐时,先查询数据库中是否已经存在该音乐
    public Music selectinsertMusic(String title, String singer);
 }
  1. xml 文件实现
    <insert id="insertMusic">
        insert into music(title, singer, time, url, userid)
        values (#{title}, #{singer}, #{time}, #{url}, #{userid})
    </insert>

    <select id="selectinsertMusic" resultType="com.example.onlinemusic.model.Music">
        select *
        from music
        where title = #{title}
          and singer = #{singer}
    </select>

知识拓展1:如何判断上传的文件是mp3

每个文件都存在其构成的方式【不能通过后缀名判断】

mp3文件格式:

在这里插入图片描述

由上图结构可知,每个Frame都由帧头和数据部分组成。具体参考以下链接:

1、https://blog.csdn.net/ffjffjffjffjffj/article/details/99691239
2、https://www.cnblogs.com/ranson7zop/p/7655474.html
3、https://blog.csdn.net/sunshine1314/article/details/2514322


知识拓展2:时间格式化的类:SimpleDateFormat

package com.example.onlinemusic.tools;

import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatTest {
    public static void main(String[] args) {
        //获取当前年月日
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
        String time=sf.format(new Date());
        System.out.println(time);

        //获取当前年月日 和 时分秒
        SimpleDateFormat sf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String time2=sf2.format(new Date());
        System.out.println(time2);
    }
}


6.5 播放音乐模块

  1. Controller 层实现
    //播放音乐:发送一个请求来获取音乐资源字节文件
    @RequestMapping("/get")
    // ResponseEntity对象是Spring对请求响应的封装。它继承了HttpEntity对象,包含了Http的响应码httpstatus)、响应头(header)、响应体(body)三个部分
    public ResponseEntity<byte[]> getMusic(String path) {
        File file = new File(Save_path + path);// 请求时注意要加上.mp3
        byte[] body = null;
        try {
            // Files.readAllBytes(String path) : 读取文件中的所有字节,读入内存 ,参数path是文件的路径
            body = Files.readAllBytes(file.toPath());
            if (body == null) {
                // 如果 body 此时还为null,说明没有读取到文件,即前端发送了一个错误的请求,没有找到对应的音乐文件
                return ResponseEntity.badRequest().build();// 错误请求,返回 400 状态码
            }
            return ResponseEntity.ok(body);//返回 200 状态码 以及对应的文件字节
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ResponseEntity.badRequest().build();
    }

代码解释:

  1. Files.readAllBytes(String path) : 读取文件中的所有字节,读入内存 ,参数path是文件的路径
  2. ResponseEntity: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);
  1. 与API中的描述一致,无参ok方法返回OK状态,有参ok方法返回body内容和OK状态
  2. body类型 是 泛型T,也就是我们不确定body是什么类型,可以向ok方法传递任意类型的值
  3. 有参ok方法其实有调用无参ok方法

参考链接:

1、使用ResponseEntity处理API返回
2、https://www.jianshu.com/p/1238bfb29ee1
3、https://blog.csdn.net/qq_43317193/article/details/100109136

难点:添加前端开源播放器控件

1.码云地址: https://gitee.com/jackzhang1204/sewise-player
2.GitHub地址:https://github.com/jackzhang1204/sewise-player
3.音频播放示例:https://gitee.com/jackzhang1204/sewise-player/blob/master/demos/audio.html

将该开源项目,下载到本地,取出player文件夹,放入static文件夹下

前端嵌入播放器:

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

实现播放:

    function playerSong(obj) {
            console.log(obj)
            var name = obj.substring(obj.lastIndexOf('=') + 1);
            //obj:播放地址 name:歌曲或者视频名称 0:播放的时间  false:点击播放按钮时不开启自动播放
            SewisePlayer.toPlay(obj, name, 0, true);
        }

6.6 删除音乐模块

当删除主页面的音乐后,收藏列表对应的音乐也要被删除掉 !!

6.6.1 删除单个音乐

  1. 请求和响应设计

请求:
{
post,
/music/delete,
id
}
响应:
{
“status”: 1,
“message”: “删除成功!”,
“data”: true
}

  1. Service 层
    public Music selectMusicById(int id){
        return musicMapper.selectMusicById(id);
    }

    public int deleteMusicById(int id){
        return musicMapper.deleteMusicById(id);
    }
  1. Mapper 层
//根据id删除音乐,先查询判断数据库中是否存在该id的音乐
    public Music selectMusicById(@RequestParam Integer id);

    //如果查询到数据库中存在该id对应的音乐,即可以删除
    public int deleteMusicById(@RequestParam Integer id);
  1. xml 文件实现
    <select id="selectMusicById" 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>
  1. Controller 层实现
//根据单个id 删除单个音乐
    //后期优化:根据id删除可能会出现数据库删除成功,服务器删除成功!但是此时数据库另一个id对应的音乐也是删除的这个音乐(音乐名字相同,歌手不同),
    //这时该idList在数据库中的音乐就没有删除,但是在服务器上已经删除了该音乐
    @RequestMapping("/delete")
    public ResponseBodyMessage<Boolean> deleteMusicByid(String id) {
        int musicId = Integer.parseInt(id);// PS:如果参数直接是int类型,就会报错,是String类型,将其转换为int 就不会报错 原因不知
        //1.先查询该数据库中该idList是否存在音乐
        Music music = userService.selectMusicById(musicId);
        if (music == null) {
            return new ResponseBodyMessage<>(-1, "数据库中没有你想删除的音乐", false);
        } else {
            //2.删除数据库中的音乐
            int ret = userService.deleteMusicById(musicId);
            if (ret == 1) {
                //3.数据库中删除成功,还要删除服务器中的音乐
                String title = music.getTitle();
                File file = new File(Save_path + title + ".mp3");
                System.out.println("此时文件的路径:" + file.getPath());
                Boolean flg = file.delete();
                if (flg) {
                    //数据库和服务器中的音乐数据被删除后,收藏表中对应的音乐也应被删除
                    userService.deleteLoveMusicById(musicId);
                    return new ResponseBodyMessage<>(1, "数据库删除成功且服务器删除成功且删除收藏表对应的音乐 !", true);
                } else {
                    return new ResponseBodyMessage<>(-1, "数据库删除成功但是服务器删除失败 !", false);
                }
            } else {
                return new ResponseBodyMessage<>(-1, "数据库删除失败 !", false);
            }
        }
    }

6.6.2 批量删除选中音乐

  1. 请求和响应设计

请求:
{
post,
/music/deletesel,
data:{“id”:id}
}
响应:
{
“status”: 1,
“message”: “批量删除成功”,
“data”: true
}

  1. Controller 层实现
//根据id批量删除  与单个删除逻辑一样,只是需要循环遍历集合来一个一个删除
    @RequestMapping("/deletebatch")
    public ResponseBodyMessage<Boolean> deleteBatchMusic(@RequestParam(value = "id[]") List<Integer> idList) {

        int sum = 0;
        for (int i = 0; i < idList.size(); i++) {
            Music music = userService.selectMusicById(idList.get(i));
            if (music == null) {
                return new ResponseBodyMessage<>(-1, "数据库中没有你想删除的音乐", false);
            } else {
                //2.删除数据库中的音乐
                int ret = userService.deleteMusicById(idList.get(i));
                if (ret == 1) {
                    //3.数据库中删除成功,还要删除服务器中的音乐
                    String title = music.getTitle();
                    File file = new File(Save_path + title + ".mp3");
                    System.out.println("此时文件的路径:" + file.getPath());
                    Boolean flg = file.delete();
                    if (flg) {
                        //数据库和服务器中的音乐数据被删除后,收藏表中对应的音乐也应被删除
                        userService.deleteLoveMusicById(idList.get(i));
                        sum += ret;
                        // 注意:下面语句放开注释,整个功能逻辑就会出错 !!  花了半小时才找出来0.0
                        //return new ResponseBodyMessage<>(1, "数据库删除成功且服务器删除成功 !", true);
                    } else {
                        return new ResponseBodyMessage<>(-1, "数据库删除成功但是服务器删除失败 !", false);
                    }
                } else {
                    return new ResponseBodyMessage<>(-1, "数据库删除失败 !", false);
                }
            }
        }
        if (sum == idList.size()) {
            return new ResponseBodyMessage<>(1, "批量删除音乐成功 !", true);
        } else {
            return new ResponseBodyMessage<>(-1, "批量删除音乐失败 !", false);
        }
    }

6.7 查询音乐模块

当输入参数时进行模糊查询,参数为空时进行全部查询

  1. 请求和响应设计

请求:
{
get,
/music/findmusic,
data:{musicName:musicName},
}
响应:【不给musicName传参】
{
“status”: 1/-1,
“message”: “”,
“data”: “”
}
响应:【给musicName传参】
{
“status”: 1/-1,
“message”: “”,
“data”: “”
}

  1. Service 层实现
    public List<Music> findMusicByMusicName(String name){
        return musicMapper.findMusicByMusicName(name);
    }

    public List<Music> findMusic(){
        return musicMapper.findMusic();
    }
  1. Mapper 层实现
 // 根据歌曲名字,查询音乐
    List<Music> findMusicByMusicName(String name);

    // 当参数为空时,查询所有音乐
    List<Music> findMusic();
  1. xml 文件实现
    <select id="findMusicByMusicName" resultType="com.example.onlinemusic.model.Music">
        select * from music where title like concat('%',#{musicName},'%')
    </select>

    <select id="findMusic" resultType="com.example.onlinemusic.model.Music">
        select * from music
    </select>

注意: 进行模糊查询时,使用like语句的写法

  1. Controller 层实现
    // 查询音乐
    @RequestMapping("/findmusic")
    //(required=false)可以不传入参数,默认为 true,不传入参数就会报错 !!                    //该参数要和前端参数对应
    public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required = false) String musicName) {
        List<Music> musicList = null;
        if (musicName != null) {
            //不为空就进行模糊查询
            musicList = userService.findMusicByMusicName(musicName);
        } else {
            //参数为空就查询全部的音乐
            musicList = userService.findMusic();
        }
        return new ResponseBodyMessage<>(1, "查询到了歌曲信息", musicList);
    }

6.8 添加音乐至喜欢的列表模块

  1. 请求和响应设计

请求:
{
post,
/lovemusic/insertlikemusic
data: id//音乐id
}
响应:
{
“status”: 1,
“message”: “点赞音乐成功”,
“data”: true
}

  1. LoveMusicMapper接口
    1、需要查询此次收藏音乐是否之前收藏过,收藏过则不能添加
    2、没有收藏过,插入数据库中一条记录
@Mapper
public interface LoveMusicMapper {

    //1.收藏音乐之前先查看该音乐是否已经收藏过了
    public Music findLoveMusicByMusicIdAndUserId(@RequestParam  Integer userId, @RequestParam Integer musicId);

    //2.没有收藏该音乐的话就可以插入该音乐到收藏表中
    public Boolean insertLoveMusic(@RequestParam Integer userId, @RequestParam Integer musicId);
 }
  1. xml 文件实现
    <select id="findLoveMusicByMusicIdAndUserId" resultType="com.example.onlinemusic.model.Music">
        select *
        from lovemusic
        where user_id = #{userId}
          and music_id = #{musicId}
    </select>

    <!-- 下面的返回类型不能是 Boolean -->
    <insert id="insertLoveMusic">
        insert into lovemusic(user_id, music_id)
        values (#{userId}, #{musicId})
    </insert>
  1. Controller 层实现
@RestController
@RequestMapping("/lovemusic")
public class LoveMusicController {

    @Resource
    private UserService userService;

    @RequestMapping("/insertlikemusic")
    public ResponseBodyMessage<Boolean> LoveMusic(@RequestParam String id, HttpServletRequest request) {
        int musicId = Integer.parseInt(id);

        HttpSession httpSession = request.getSession(false);
        if (httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
            return new ResponseBodyMessage<>(-1, "您还没有登录 !", false);
        } else {
            User user = (User) httpSession.getAttribute(Constant.USERINFO_SESSION_KEY);
            int userId = user.getId();

            //TODO:还可以扩展,该音乐是否已经上传,只有上传成功的音乐才能收藏

            //先查询该音乐是否已经被收藏
            Music music = userService.findLoveMusicByMusicIdAndUserId(userId, musicId);
            if (music != null) {
                System.out.println("该音乐已经被收藏 !");
                return new ResponseBodyMessage<>(-1, "该音乐已经被收藏,重复收藏失败 !", false);
            } else {
                //没有收藏过就可以进行收藏操作
                Boolean flg = userService.insertLoveMusic(userId, musicId);
                if (flg) {
                    return new ResponseBodyMessage<>(1, "收藏音乐成功 !", true);
                } else {
                    return new ResponseBodyMessage<>(-1, "收藏音乐失败 !", false);
                }
            }
        }
    }

6.9 查询收藏的音乐模块

功能实现和之前在主页面的查询一样 !! 注意细节修改

  1. 请求和响应设计

请求:
{
get,
/lovemusic/findlovemusic,
data:{musicName:musicName},
}
响应:【不给musicName传参】
{
“status”: 1/-1,
“message”: “”,
“data”: “”
}
响应:【给musicName传参】
{
“status”: 1/-1,
“message”: “”,
“data”: “”
}

  1. Mapper 层实现
    //查询用户收藏的音乐:如果没有传入具体的歌曲名,显示当前用户收藏的所有音乐
    public List<Music> findLoveMusicByUserId(int userId);

    //根据某个用户的ID和歌曲名称查询某个用户收藏的音乐,支持模糊查询
    public List<Music> findLoveMusicBykeyAndUID(String musicName, int userId);
  1. xml 文件实现(重要)
    <select id="findLoveMusicByUserId" resultType="com.example.onlinemusic.model.Music">
        select m.*
        from lovemusic lm,
             music m
        where m.id = lm.music_id
          and lm.user_id = #{userId}
    </select>

    <select id="findLoveMusicBykeyAndUID" resultType="com.example.onlinemusic.model.Music">
        select m.*
        from lovemusic lm,
             music m
        where m.id = lm.music_id
          and lm.user_id = #{userId}
          and title like concat('%', #{musicName}, '%')
    </select>
  1. Controller 层
    这里就不重复赘述了,大家参考前面 !!

6.10 删除收藏的音乐模块

  1. 请求和响应设计

请求:
{
post,
/lovemusic/deletelovemusic,
data:{id:id}
}
响应:
{
“status”: 1,
“message”: “取消收藏成功!”,
“data”: true
}

  1. Service 层
    将上述 Service 层代码补齐
    public Music findLoveMusicByMusicIdAndUserId(Integer userId,Integer musicId){
        return loveMusicMapper.findLoveMusicByMusicIdAndUserId(userId,musicId);
    }

    public Boolean insertLoveMusic(Integer userId,Integer musicId){
        return loveMusicMapper.insertLoveMusic(userId,musicId);
    }

    public List<Music> findLoveMusicByUserId(int userId){
        return loveMusicMapper.findLoveMusicByUserId(userId);
    }

    public List<Music> findLoveMusicBykeyAndUID(String musicName, int userId){
        return loveMusicMapper.findLoveMusicBykeyAndUID(musicName,userId);
    }

    public int deleteLoveMusic(int userId,int musicId){
        return loveMusicMapper.deleteLoveMusic(userId,musicId);
    }

    public int deleteLoveMusicById(int musicId){
        return loveMusicMapper.deleteLoveMusicById(musicId);
    }
  1. Mapper 层
    //移除自己收藏的音乐,但是不是删除音乐本身,只是从数据库中删除了记录而已
    public int deleteLoveMusic(int userId,int musicId);

    //删除音乐完善:当删除库中的音乐的时候,同步删除lovemusic中的数据
    public int deleteLoveMusicById(int musicId);
  1. xml 文件实现
    <delete id="deleteLoveMusic" parameterType="java.lang.Integer">
        delete from lovemusic where user_id = #{userId} and music_id = #{musicId}
    </delete>

    <delete id="deleteLoveMusicById" parameterType="java.lang.Integer">
        delete from lovemusic where music_id=#{musicId}
    </delete>

七、配置拦截器

  1. 新建 LoginInterceptor ,实现HandlerInterceptor 接口
package com.example.onlinemusic.config;

import com.example.onlinemusic.tools.Constant;
import com.example.onlinemusic.tools.ResponseBodyMessage;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 没有获取到 Session 不会创建
        HttpSession httpSession = request.getSession(false);
        // 1.验证是否登录,登录成功后才允许上传
        if (httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
            System.out.println("没有登录!");
            response.sendRedirect("/login.html");
            return false;
        }
        return true;
    }
}

  1. 将拦截器注册到系统配置文件,并设置拦截规则
package com.example.onlinemusic.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LoginInterceptor loginInterceptor = new LoginInterceptor();
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//拦截所有接口
                //排除所有的JS
                .excludePathPatterns("/js/**.js")
                //排除images下所有的元素
                .excludePathPatterns("/images/**")
                .excludePathPatterns("/css/**.css")
                .excludePathPatterns("/fronts/**")
                .excludePathPatterns("/player/**")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/login.html")
                //排除登录注册接口
                .excludePathPatterns("/user/login1")
                .excludePathPatterns("/user/reg");//注意里面的格式 / 不要掉

    }
}


八、测试用例(了解即可)

在这里插入图片描述

九、项目部署

前提:拥有一台服务器,在服务器上安装必须的文件如,JDK,Mysql 等

  1. 将数据库在服务器上重新进行建表等操作,相应的数据也需要重新获取
    在这里插入图片描述

  2. 修改项目中的路径,数据库密码等,匹配服务器即可
    在这里插入图片描述

  3. 将打包好的jar包,通过xftp 上传到服务器上相应的文件目录下
    在这里插入图片描述

  4. 使用java -jar xxxx.jar 启动项目【前台运行的方式】,再通过自己的外网IP访问

  5. 后台运行springboot项目:nohup java -jar xxx.jar >> log.log &

nohup:后台运行项目的指令
使用 >> log.log 将运行的日志记录到 log.log中
& 表示 一直运行

  1. 进行相应的功能检查,并修改
  2. springboot项目更新

1.先通过 ps -ef | grep java 查询当前的springboot项目的进程,然后 kill 【进程ID】kill掉
命令说明:
ps : Linux 当中查看进程的命令
-e 代表显示所有的进程
-f 代表全格式【显示全部的信息】
grep : 全局正则表达式
2.将原服务器上的jar包删除掉,重新上传新的jar包
3.重新进行后台的启动


十、项目拓展方向

  1. 检测文件是否为mp3文件
  2. 实现对音乐的评论,对评论的点赞和回复功能
  3. 新增MV功能
  4. 新增创建歌单的功能
  5. 自己实现一个流媒体服务器,使用流媒体协议对歌曲进行播放

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/512334.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ详解(六):RabbitMQ集群搭建

集群 官方参考文档&#xff1a;https://www.rabbitmq.com/clustering.html RabbitMQ这款消息队列中间件产品本身是基于Erlang编写&#xff0c;Erlang语言天生具备分布式特性&#xff08;通过同步Erlang集群各节点的magic cookie来实现&#xff09;。因此&#xff0c;RabbitMQ天…

什么是柔性玻璃?

柔性玻璃(Flexible glass)是一种新型薄膜玻璃基板(Thin film glass substrate)材料&#xff0c;厚度极薄可以弯曲。 柔性玻璃定义有广义和狭义之分&#xff1a; 广义柔性玻璃泛指所有制成微米尺寸具有可弯曲特性的玻璃材料&#xff0c;如玻璃纤维、光纤、玻璃棉、玻璃布等。这些…

第10课【STM32 USB通讯协议实战】HID键盘+CDC虚拟串口组合设备

目录 前言USB设备类别未定义设备设备描述符/配置描述符分析如何配置从机类型如何配置设备专用的描述符如何配置从机端点 HID设备特点设备描述符/配置描述符分析HID报文描述符短条目前缀可选数据表现形式 层次结构实例分析总结 CDC设备特点设备描述符/配置描述符分析设备类特定请…

【LED子系统】四、核心层详解(一)

个人主页&#xff1a;董哥聊技术 我是董哥&#xff0c;嵌入式领域新星创作者 创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01; 文章目录 1、前言2、leds_init分析2.1 相关数据结构2.1.1 class 2.2 实现流程 3、leds_class_dev_pm_ops分析…

Mysql出现问题:ERROR 1062 (23000): Duplicate entry ‘‘ for key ‘PRIMARY‘解决方案

回城传送–》《数据库问题解决方案》 ❤️作者主页:小虚竹 ❤️作者简介:大家好,我是小虚竹。Java领域优质创作者🏆,CSDN博客专家🏆,华为云享专家🏆,掘金年度人气作者🏆,阿里云专家博主🏆,51CTO专家博主🏆 ❤️技术活,该赏 ❤️点赞 👍 收藏 ⭐再看,养成…

QT中的模态对话框及非模态对话框

QT中的模态对话框及非模态对话框 [1] QT中的模态对话框及非模态对话框[2] Qt工作笔记-主界面往模式对话框emit信号&#xff0c;有注意的问题正常情况下&#xff1a;不正常情况下&#xff1a;下面给出正常情况下的代码&#xff1a; [1] QT中的模态对话框及非模态对话框 原文链接…

KVM软件安装/Guest OS图形模式安装

KVM软件安装 首先你的Linux操作系统得带有图形化界面 虚拟机开启硬件虚拟化 关闭防火墙和selinux [rootserver-d ~]# systemctl stop firewalld [rootserver-d ~]# systemctl disable firewalld Removed symlink /etc/systemd/system/multi-user.target.wants/firewalld.ser…

RK3568修改调试串口的波特率

概述 使用了临滴 RK3568 开发板,其调试串口的默认波特率是 1500000 &#xff0c;但并不是所有的 USB 转 TTL 都能使用这么高的波特率&#xff0c;所以我们就将波特率修改为 115200 这个比较通用的波特率。 RK3568 调试串口修改波特率的方法 ddr 运行阶段串口波特率的修改 ddr…

linux利用定时任务提权

背景&#xff1a; 运维为了防止数据丢失等&#xff0c;写个定时任务进行数据的打包压缩。由于数据打包压缩命令为tar&#xff0c;tar可以尝试加参数调用其他命令执行。 压缩命令&#xff1a;tar zxf 1.tar.gz /var/www/* 查看定时任务&#xff1a;cat /etc/crontab root权限下…

WordPress入门之WordPress站点基本设置

在Wordpress站点搭建过程中,我们需要快速去熟悉Wordpress,并进行一些简单的基本设置,在开始设置之前,大家可以先熟悉左边的菜单栏的每个选项,了解它们都是做什么的,今天就简单为大家介绍Wordpress入门之Wordpress站点基本设置。 一、设置个人资料 建议大家完善基本信息…

电容笔一定要防误触吗?苹果平板平替电容笔排行

至于用ipad作为学习工具的学生们&#xff0c;更是将它当成了一种必不可少的工具。但是&#xff0c;由于苹果原装电容笔的价格过高&#xff0c;没有人能负担得起。所以&#xff0c;最好的办法就是使用普通的电容笔。我是IPAD的忠实用户&#xff0c;也是数码爱好者&#xff0c;这…

10.BOM浏览器对象模型

BOM 浏览器对象模型 1. BOM 概述 1.1 什么是 BOM BOM&#xff08;Browser Object Model&#xff09;即浏览器对象模型&#xff0c;它提供了独立于内容而与**浏览器窗口进行交互的对象&#xff0c;其核心对象是 window BOM 由一系列相关的对象构成&#xff0c;并且每个对象都…

在Bamboo上怎么使用iOS的单元测试 | 京东云技术团队

作者&#xff1a;京东零售 吴滔 本教程将使用北汽登录模块为例&#xff0c;一步一步和大家一起搭建单元测试用例&#xff0c;并在Bamboo上跑起来&#xff0c;最终测试结果和代码覆盖率会Bamboo上汇总。 模块名称&#xff1a;BQLoginModule,是通过iBiu创建的一个模块工程 一 建…

浅尝Kubernetes

第一节 内容编排与Kubernetes 为什么要用k8s 集群环境容器部署的困境&#xff0c;假设我们有数十台服务器。分别部署Nginx&#xff0c;redis&#xff0c;mysql&#xff0c;业务服务。如何合理的分配这些资源。这里就需要用到容器编排 容器编排 在实际集群环境下&#xff0…

JAVA-抽象类和接口

文章目录 前言 大家好呀,今天给大家带来抽象类和接口的讲解,那么废话不多说,跟着我一起去学习吧! 1.1抽象类的概念 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是反过来&#xff0c;并不是所有的类都是用来描绘对象的&#xff0c;如果 一个类…

Java 11新特性:模块化系统和本地变量类型推断

作为Java语言的最新版本&#xff0c;Java 11带来了许多新特性&#xff0c;其中最引人注目的是模块化系统和本地变量类型推断。这两个新特性对Java开发人员来说具有重要意义&#xff0c;因此在本文中&#xff0c;我们将详细探讨这两个新特性及其对Java开发的影响。 章节1&#…

记录一次uniapp实现APP自动升级

描述 app的版本管理和升级&#xff0c;是一个不可或缺的功能&#xff0c;而uniapp则是提供了一整套的流程&#xff0c;由于官方文档过于复杂&#xff0c;而且写的云里雾里的&#xff0c;所以个人记录一次我的操作&#xff0c;直到配置成功。 总体 一共分为2个部分&#xff0…

scanf与printf函数的用法

前言&#xff1a; 学习c语言编程&#xff0c;必不可少的操作就是键盘输入与屏幕输出。今天我想讲讲自己对scanf与printf使用的看法 一、scanf与printf&#xff1a; 1.scanf()函数&#xff1a; int scanf ( const char * format, ... ); 函数的功能&#xff1a;从标准输入中…

契约锁亮相华为2023中国合作伙伴大会,共谱华为云软件生态新篇章

5月8日-9日&#xff0c;以“因聚而生 众志有为”为主题的“华为中国合作伙伴大会2023”在深圳举行。契约锁作为华为云优秀软件伙伴&#xff0c;受邀参会并在“聚数字化先行者&#xff0c;谱软件生态新乐章”分论坛开展“让数字可信&#xff0c;打通数字化最后一公里”数字化实践…

Google Play 政策更新重点回顾 (下) | 2023 年 4 月

Google Play 和您一样&#xff0c;始终坚持为用户提供更优质的产品、更流畅的体验。为持续打造值得信赖的优质应用平台&#xff0c;我们也在不断进行政策更新。在上一篇文章中与您回顾了 2023 年 4 月 Google Play 政策的更新要点&#xff0c;这篇文章我们将为您带来重点政策解…