anita的音乐空间(项目)

2023-11-17

目录

项目核心功能

项目前置工作

1.创建项目

2.数据库设计

3.配置文件中配置数据库和xml

核心功能设计

1.登录功能

2.注册功能

3.上传音乐至音乐列表功能

4.播放音乐功能

5.删除音乐列表音乐功能

5.1删除音乐列表单个音乐功能

5.2批量删除音乐列表选中音乐功能

6.查询音乐列表音乐功能

​编辑

7.把音乐列表音乐添加到收藏音乐列表功能

8.查询收藏音乐列表的音乐功能

9.删除收藏音乐列表的音乐功能

 10.对于删除音乐功能的完善

前端页面和逻辑

1.登录页面和登录逻辑

2.注册页面和逻辑 

3.上传音乐页面和逻辑

4.音乐列表页面和逻辑

4.1页面

4.2逻辑

5.收藏音乐列表页面和逻辑

到此前端页面的代码与逻辑也完成了

最后收尾----配置拦截器

到此,完结撒花


项目核心功能

项目前置工作

1.创建项目

2.数据库设计



---创建数据库anitamusicspace
drop database if exists `anitamusicspace`;
create database if not exists `anitamusicspace` character set utf8;

---使用数据库
use `anitamusicspace`;

---创建表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
);
---在user表中插入数据(我),密码是123456的加密
INSERT INTO user(username,password)
VALUES("anita","$2a$10$Bs4wNEkledVlGZa6wSfX7eCSD7wRMO0eUwkJH0WyhXzKQJrnk85li");



---创建表music(title字段为歌曲名称,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
);
--url:将来播放音乐的时候需要请求的地址


---创建中间表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
);

3.配置文件中配置数据库和xml

##小梅需要修改端口号
server.port=8081


#配置数据库(两者选其一)
#1.这是dev的
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/anitamusicspace?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=111111
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
# 配置springboot日志调试模式是否开启
debug=true


# 设置打印日志的级别,及打印sql语句
#日志级别:trace,debug,info,warn,error
#基本日志
logging.level.root=INFO
logging.level.com.example.anitamusicspace.mapper=debug
#扫描的包:druid.sql.Statement类和frank包
logging.level.druid.sql.Statement=DEBUG
logging.level.com.example=DEBUG









核心功能设计

1.登录功能

在com.example.anitamusicspace.model包中创建User类


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

1.在com.example.anitamusicspace.mapper包中创建UserMapper接口

2.在resource下创建mybatis文件夹,在这个文件夹中创建UserMapper.xml


@Mapper
public interface UserMapper {
    User selectByName(String username);
}
<?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.anitamusicspace.mapper.UserMapper">
    <select id="selectByName" resultType="com.example.anitamusicspace.model.User">
        select * from user where username=#{username}
    </select>
</mapper>

 在com.example.anitamusicspace.service包中创建UserService类

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;


    public User selectByName(String username){
        return userMapper.selectByName(username);
    }
}

1.在com.example.anitamusicspace.controller包中创建UserController类

2.为了方便,我在com.example.anitamusicspace.tools中设置了:

(1)对响应体进行了统一设计

(2)对seesion对象进行了常量设置

3.登录涉及到了BCrypt加密和拦截器

(1)BCrypt加密需要:1.添加依赖 2.在springboot启动类添加一段代码 3.创建BCryptTest测试类 4.在com.example.anitamusicspace.config包中创建AppConfig类

(2)拦截器项目等最后设置

补充:

关于加密的介绍:关于加密---BCrypt和MD5_在上山的mei的博客-CSDN博客

对响应体进行了统一设计


package com.example.anitamusicspace.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;
    }
}

对seesion对象进行了常量设置 

package com.example.anitamusicspace.tools;

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

添加依赖

<!-- 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中,默认的Spring Security生效了的,此时的接口都是被保护的,我们需要通过验证才能正常的访问。此时通过上述配置,即可禁用默认的登录验证

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

 创建BCryptTest测试类

package com.example.anitamusicspace.tools;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BCryptTest {
    public static void main(String[] args) {
        //模拟从前端获得的密码
        String password = "123456";
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String newPassword = bCryptPasswordEncoder.encode(password);
        System.out.println("加密的密码为: "+newPassword);
        //使用matches方法进行密码的校验
        boolean same_password_result = bCryptPasswordEncoder.matches(password,newPassword);
        //返回true
        System.out.println("加密的密码和正确密码对比结果: "+same_password_result);
        boolean other_password_result = bCryptPasswordEncoder.matches("987654",newPassword);
        //返回false
        System.out.println("加密的密码和错误的密码对比结果: " + other_password_result);
    }
}

创建AppConfig类 

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

创建UserController类 

@RestController//@RestController:@ResponseBody + @Controller合在一起的作用。@Controller注解,表明了这个类是一个控制器类 ,@ResponseBody表示方法的返回值直接以指定的格式写入Http response body中
@RequestMapping("/user")//使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求
public class UserController {
    @Autowired
    private UserService userService;


    //登录
    @Autowired//在自动装配之前,需要完成注入,之后在AppConfig中进行注入
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @RequestMapping("/login")
    public ResponseBodyMessage<User> login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) {
        //关于@RequestParam的解释:将请求参数绑定到你控制器的方法参数上 。如果这个参数是非必传的可以写为@RequestParam(required = false) ,默认是true
        User userInfo=userService.selectByName(username);
        if(userInfo == null){
            return new ResponseBodyMessage<>(-1, "用户名或者密码错误", userInfo);
        }
        else{
            boolean flg=bCryptPasswordEncoder.matches(password,userInfo.getPassword());
            if(!flg){
                return new ResponseBodyMessage<>(-1, "用户名或者密码错误", userInfo);
            }
            request.getSession().setAttribute(Constant.USERINFO_SESSION_KEY,userInfo);//session的创建
            return new ResponseBodyMessage<>(0, "登录成功", userInfo);
        }
    }
}

2.注册功能

注册这里的逻辑是:先根据要注册的username查询,如果数据库的user表中有这个username那么就不能注册,注册失败;如果没有就进行插入操作,注册成功

1.在UserMapper接口中添加代码

2.在UserMapper.xml中添加代码

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

 在UserService中添加代码

public int register(User registerUser){
        return userMapper.register(registerUser);
}

 在UserController中添加代码

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

3.上传音乐至音乐列表功能

上传音乐的逻辑:

1.首先需要确认是否登录,如果没有需要先登录

2.查询数据库中是否有【歌曲名+歌手名】与要上传的【歌曲名+歌手名】一样的

(1)有就不能上传

(2)没有,那么就按照先上传到服务器(需要在配置文件中添加上传音乐的路径)--->判断上传的文件格式是否正确--->符合格式再上传到数据库,不符合就从服务器中删除并且不上传到数据库的逻辑顺序进行音乐的上传

补充:

1.关于MultipartFile类的参考介绍:深入理解MultipartFile,以更优雅的方式处理文件_Steafan_的博客-CSDN博客_multipartfile

2.关于MP3文件的介绍和参考:

mp3文件格式详解_oj_fang的博客-CSDN博客_mp3文件格式

MP3格式音频文件结构解析 - nigaopeng - 博客园

MP3文件格式解析_lspbeyond的博客-CSDN博客_mp3格式解析

3.由于music表中有一栏是上传时间,因此介绍下上传时间的格式:

   时间格式化的类--->SimpleDateFormat

SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");//比如2022-08-01
String time=sf.format(new Date());

SimpleDateFormat sf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//比如2022-08-01 20:00:00
String time2=sf2.format(new Date());
    

 在com.example.anitamusicspace.model包中创建Music类


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

1.在com.example.anitamusicspace.mapper包中创建MusicMapper接口

2.在resource下创建mybatis文件夹,在这个文件夹中创建MusicMapper.xml

@Mapper
public interface MusicMapper {
    //往数据库中上传音乐
    int insert(String title,String singer,String time,String url,int userid);
    //查询数据库中是否有要上传的音乐,如果数据库中有两次音乐就不再上传第二遍
    List<Music> findMusicByMusicNameAndSinger(String title);

}
<?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.anitamusicspace.mapper.MusicMapper">
<!--    往数据库中上传音乐-->
    <insert id="insert">
        insert into music(title,singer,time,url,userid)
        values(#{title},#{singer},#{time},#{url},#{userid})
    </insert>
<!--    查询数据库中是否有要上传的音乐,如果数据库中有两次音乐就不再上传第二遍-->
    <select id="findMusicByMusicNameAndSinger" resultType="com.example.anitamusicspace.model.Music">
        select * from music where title=#{title}
    </select>

 在com.example.anitamusicspace.service包中创建MusicService类

@Service
public class MusicService {
    @Resource
    private MusicMapper musicMapper;

    public int insert(String title,String singer,String time,String url,int userid){
        return musicMapper.insert(title,singer,time,url,userid);
    }

    public List<Music> findMusicByMusicNameAndSinger(String title){
        return musicMapper.findMusicByMusicNameAndSinger(title);
    }
}

1.在com.example.anitamusicspace.controller包中创建MusicController类 

2.在配置文件中配置上传音乐的路径,并在MusicController中进行注入

@RestController
@RequestMapping("/music")
public class MusicController {
    @Value("${music.local.path}")
    private String SAVE_PATH;

    @Autowired
    private MusicService musicService;


    //上传音乐
    @RequestMapping("/upload")
    public ResponseBodyMessage<Boolean> insertMusic(@RequestParam String singer, @RequestParam("filename")
            MultipartFile file, HttpServletRequest req, HttpServletResponse resp) throws IOException {
        //1.检查是否登录了
        HttpSession httpSession = req.getSession(false);
        if (httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
            System.out.println("没有登录,请先登录!");
            return new ResponseBodyMessage<>(-1, "没有登录,请先登录!", false);
        }


        //2.查询数据库中是否有当前音乐[歌曲名和歌手都一样],有就不再上传
        String filenameAndType = file.getOriginalFilename();//xxx.mp3
        System.out.println("filenameAndType--->>>>>>>>>>>>>>>>>" + filenameAndType);
        int index = filenameAndType.lastIndexOf(".");
        String title = filenameAndType.substring(0, index);
        List<Music> list = musicService.findMusicByMusicNameAndSinger(title);
        if(list != null){
            for(Music music : list) {
                if(music.getSinger().equals(singer)){
                    return new ResponseBodyMessage<>(-1,"当前歌手的歌曲已经存在!",false);
                }
            }
        }


        //3.上传到服务器
        String path = SAVE_PATH + "/" + filenameAndType;
        File dest = new File(path);
        System.out.println("dest:=>" + dest.getPath());
        if (!dest.exists()) {
            dest.mkdirs();
        }
        try {
            file.transferTo(dest);//上传文件到目标
        } catch (IOException e) {
            e.printStackTrace();
            return new ResponseBodyMessage<>(-1, "服务器上传失败!", false);
        }
        //判断上传的文件是不是mp3文件,不是的话就从服务器删掉
        File ff=new File(path);
        byte[] res = null;
        try {
            res = Files.readAllBytes(ff.toPath());
            String str = new String(res);
            if(!str.contains("TAG")) {
                ff.delete();
                return new ResponseBodyMessage<>(-1,"当前上传的不是mp3文件,请上传mp3文件",false);
            }
        } catch (IOException e) {
            e.printStackTrace();
            return new ResponseBodyMessage<>(-1,"服务器出现问题", false);
        }


        //4.进行数据库的上传
        //(1).准备数据 (2).调用insert
        User user = (User) httpSession.getAttribute(Constant.USERINFO_SESSION_KEY);
        int userid = user.getId();
        String url = "/music/get?path=" + title;
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");//如果想获取的是精确到时分秒,yyyy-MM-dd HH:mm:ss
        String time = sf.format(new Date());

        try {
            int ret = 0;
            ret = musicService.insert(title, singer, time, url, userid);
            if (ret == 1) {
       

                return new ResponseBodyMessage<>(0, "数据库上传成功!", true);
            } else {
                return new ResponseBodyMessage<>(-1, "数据库上传失败!", false);
            }
        } catch (BindingException e) {
            dest.delete();
            return new ResponseBodyMessage<>(-1, "数据库上传失败!", false);
        }
    }


   
#音乐上传后的路径
#1.这是dev的
music.local.path=C:/Java/javacode/bigproject

4.播放音乐功能

关于ResponseEntity的介绍:

ResponseEntity的基本简介_胖橘爱吃小白粥的博客-CSDN博客_responseentity

https://www.jianshu.com/p/1238bfb29ee1

使用ResponseEntity处理API返回_半城风花半城雨的博客-CSDN博客

在MusicController中添加代码

@RequestMapping("/get")
    public ResponseEntity<byte[]> get(String path) {
        File file=new File(SAVE_PATH+"/"+path);
        byte[] a = null;
        try {
            a = Files.readAllBytes(file.toPath());
            if(a==null){
                return ResponseEntity.badRequest().build();
            }
            return ResponseEntity.ok(a);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ResponseEntity.badRequest().build();

5.删除音乐列表音乐功能

删除功能的逻辑:先通过id查询要删除的音乐是否存在,存在就根据此id删除音乐。然后删除顺序是先删除数据库中此音乐,再删除服务器上此音乐

5.1删除音乐列表单个音乐功能

1.在MusicMapper接口中添加代码

2.在MusicMapper.xml中添加代码

//1.先查找要删除的音乐是否存在(通过id查看)
    Music findMusicById(int id);
//2.如果存在就删除当前id的音乐
    int deleteMusicById(int musicId);
<!--    查询是否存在该id的音乐-->
    <select id="findMusicById" resultType="com.example.anitamusicspace.model.Music">
        select * from music where id=#{id}
    </select>
<!--    存在就根据该id进行删除-->
    <delete id="deleteMusicById" parameterType="java.lang.Integer">
        delete from music where id=#{id}
    </delete>

在MusicService中添加代码

public Music findMusicById(int id){
        return musicMapper.findMusicById(id);
}

public int deleteMusicById(int musicId){
        return musicMapper.deleteMusicById(musicId);
}

 在MusicController中添加代码

@RequestMapping("/delete")
    public ResponseBodyMessage<Boolean> deleteMusicById(@RequestParam String id) {
        int iid = Integer.parseInt(id);
        Music music = musicService.findMusicById(iid);
        //看要删除的音乐的id是否存在
        //不存在就提示没有要删除的音乐
        if(music == null){
            return new ResponseBodyMessage<>(-1,"没有你要删除的音乐",false);
        }
        //存在就要删除数据库和服务器上此音乐的数据
        int ret = musicService.deleteMusicById(iid);
        //ret=1就代表数据库中删除了一条,因此接下来接着删服务器上的数据
        if(ret == 1) {
            int index = music.getUrl().lastIndexOf("=");
            String filename = music.getUrl().substring(index+1);
            File file = new File(SAVE_PATH+"/"+filename+".mp3");
            if(file.delete()) {
                //同步删除lovemusic表中的数据
                loveMusicService.deleteLoveMusicById(iid);
                return new ResponseBodyMessage<>(0,"删除服务器音乐成功",true);
            }else {
                return new ResponseBodyMessage<>(-1, "删除服务器音乐失败", false);
            }
            //ret!=1就代表数据库没删除成功
        }else{
            return new ResponseBodyMessage<>(-1,"删除数据库中的音乐失败",false);
        }
    }

5.2批量删除音乐列表选中音乐功能

  在MusicController中添加代码

//删除音乐----一次删除多个音乐
    @RequestMapping("/deleteSel")
    public ResponseBodyMessage<Boolean> deleteSelMusic(@RequestParam("id[]") List<Integer> id) {
        int sum = 0;
        for (int i = 0; i < id.size(); i++) {
            int musicId = id.get(i);
            Music music = musicService.findMusicById(musicId);
            int ret = musicService.deleteMusicById(musicId);
            if(ret == 1) {
                int index = music.getUrl().lastIndexOf("=");
                String filename = music.getUrl().substring(index+1);
                File file = new File(SAVE_PATH+"/"+filename+".mp3");
                if(file.delete()) {
                    //同步删除lovemusic表中的数据
                    loveMusicService.deleteLoveMusicById(musicId);
                    sum += ret;
                }else {
                    System.out.println("删除失败!");
                    return new ResponseBodyMessage<>(-1,"删除服务器上的音乐失败",false);
                }
            }else {
                System.out.println("删除失败!");
                return new ResponseBodyMessage<>(-1,"删除数据库中的音乐失败",false);
            }
        }
        if(sum == id.size()) {
            System.out.println("删除成功!");
            return new ResponseBodyMessage<>(0,"批量删除成功",true);
        }else {
            System.out.println("删除失败!");
            return new ResponseBodyMessage<>(0,"批量删除成功",false);
        }
    }

6.查询音乐列表音乐功能

查询音乐功能满足:

1.支持模糊查询:select * from music where title like concat('%',#{musicName},'%')

2.支持传入参数为空:@RequestParam(required=false)

1.在MusicMapper接口中新增代码

2.在MusicMapper.xml中添加代码

//查询音乐
    //查询音乐分为两种
    //一种是不给musicName传参,默认查询所有音乐
    List<Music> findMusic();

    //另一种是给musicName传参
    List<Music> findMusicByMusicName(String name);
<!--    查询音乐-->
    <!--   这个是没有传参的 -->
    <select id="findMusic" resultType="com.example.anitamusicspace.model.Music">
        select * from music
    </select>
    <!--   这个是传参的 -->
    <select id="findMusicByMusicName" resultType="com.example.anitamusicspace.model.Music">
        select * from music where title like concat('%',#{musicName},'%')
    </select>

 在MusicService类中新增代码

public List<Music> findMusic(){
        return musicMapper.findMusic();
}


public List<Music> findMusicByMusicName(String name){
        return musicMapper.findMusicByMusicName(name);
}

在MusicController类中新增代码

//查询音乐
    @RequestMapping("/findmusic")//(required=false)可以不传入参数
    public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required=false) String musicName) {
        List<Music> musicList = null;
        if(musicName != null) {
            musicList = musicService.findMusicByMusicName(musicName);
        }else {
            //默认查询全部的音乐
            musicList = musicService.findMusic();
        }
        return new ResponseBodyMessage<>(0,"查询到了歌曲的信息",musicList);
    }

7.把音乐列表音乐添加到收藏音乐列表功能

此功能的逻辑是:

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

1.在com.example.anitamusicspace.mapper包中创建LoveMusicMapper接口

2.在resource下创建mybatis文件夹,在这个文件夹中创建LoveMusicMapper.xml

@Mapper
public interface LoveMusicMapper {



    //添加收藏音乐
    //需要查询此次收藏音乐是否之前收藏过,收藏过则不能添加
    Music findLoveMusicByMusicIdAndUserId(int userId, int musicId);

    //没有收藏过,插入数据库中一条记录
    boolean insertLoveMusic(int userId, int musicId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.anitamusicspace.mapper.LoveMusicMapper">



<!--收藏音乐,根据用户ID和音乐ID查询收藏的音乐 -->
    <select id="findLoveMusicByMusicIdAndUserId" resultType="com.example.anitamusicspace.model.Music">
    select * from lovemusic where user_id=#{userId} and music_id=#{musicId}
    </select>
    <insert id="insertLoveMusic">
    insert into lovemusic(user_id,music_id) values(#{userId},#{musicId})
    </insert>
</mapper>

 在com.example.anitamusicspace.service包中创建LoveMusicService类

@Service
public class LoveMusicService {
    @Resource
    private LoveMusicMapper loveMusicMapper;

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

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

 在com.example.anitamusicspace.controller包中创建LoveMusicController类

@RestController
@RequestMapping("/lovemusic")
public class LoveMusicController {
    @Resource
    private LoveMusicService loveMusicService;

    //添加到收藏音乐
    @RequestMapping("/likeMusic")
    public ResponseBodyMessage<Boolean> likeMusic(@RequestParam String id, HttpServletRequest req) {
        int musicId = Integer.parseInt(id);
        System.out.println("likeMusic->musicId: "+musicId);
        //判断是否登录
        HttpSession httpSession = req.getSession(false);
        if(httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
            System.out.println("没有登录!");
            return new ResponseBodyMessage<>(-1,"没有登录",false);
        }
        User user = (User)httpSession.getAttribute(Constant.USERINFO_SESSION_KEY);
        int userId = user.getId();
        System.out.println("likeMusic->userID: "+userId);
        //查询当前用户是否点赞过该音乐
        Music music = loveMusicService.findLoveMusicByMusicIdAndUserId(userId,musicId);
        if(music!=null) {
            //之前收藏过,就不能再收藏了
            return new ResponseBodyMessage<>(-1,"该用户点赞过该音乐",false);
        }else {
            boolean effect = loveMusicService.insertLoveMusic(userId,musicId);
            if(effect) {
                return new ResponseBodyMessage<>(0,"点赞音乐成功",true);
            }else {
                return new ResponseBodyMessage<>(-1,"点赞音乐失败",false);
            }
        }
    }

8.查询收藏音乐列表的音乐功能

此功能需满足:

1. 支持模糊查询
2. 支持传入参数为空

1.在LoveMusicMapper接口中新增代码

2.在LoveMusicMapper.xml中添加代码

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

    //根据某个用户的ID和歌曲名称查询,某个用户收藏的音乐
    List<Music> findLoveMusicBykeyAndUID(String musicName, int userId);
<!--    查询收藏的音乐-->
    <select id="findLoveMusicByUserId" resultType="com.example.anitamusicspace.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.anitamusicspace.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>

 在LoveMusicService类中新增代码

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

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

 在LoveMusicController类中新增代码

//查询收藏了的音乐
    @RequestMapping("/findlovemusic")
    public ResponseBodyMessage<List<Music>> findLoveMusic(@RequestParam(required=false)String musicName,
                                                          HttpServletRequest req) {
        //判断是否登录
        HttpSession httpSession = req.getSession(false);
        if (httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
            System.out.println("没有登录!");
            return new ResponseBodyMessage<>(-1, "没有登录", null);
        }
        User user = (User) httpSession.getAttribute(Constant.USERINFO_SESSION_KEY);
        int userid = user.getId();
        List<Music> musics = new ArrayList<>();
        if (musicName != null) {
            musics = loveMusicService.findLoveMusicBykeyAndUID(musicName, userid);
        } else {
            musics = loveMusicService.findLoveMusicByUserId(userid);
        }
        return new ResponseBodyMessage<>(0,"查询到了所有的收藏的音乐",musics);
    }

9.删除收藏音乐列表的音乐功能

注意此功能:删除收藏音乐仅删除的是收藏音乐列表中的音乐,不删除原音乐列表中的此音乐

1.在LoveMusicMapper接口中新增代码

2.在LoveMusicMapper.xml中添加代码

//取消音乐的收藏(把指定音乐从收藏音乐中删除)
        int deleteLoveMusic(int userId,int musicId);
<!--    取消收藏的音乐-->
    <delete id="deleteLoveMusic" parameterType="java.lang.Integer">
    delete from lovemusic where user_id=#{userId} and music_id=#{musicId}
    </delete>

在LoveMusicService类中新增代码 

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

在LoveMusicController类中新增代码 

 //取消音乐的收藏
        @RequestMapping("/deletelovemusic")
        public ResponseBodyMessage<Boolean> deleteLoveMusic(@RequestParam String id, HttpServletRequest req) {
            int musicId = Integer.parseInt(id);
            //判断是否登录
            HttpSession httpSession = req.getSession(false);
            if(httpSession == null || httpSession.getAttribute(Constant.USERINFO_SESSION_KEY) == null) {
                System.out.println("没有登录!");
                return new ResponseBodyMessage<>(-1,"没有登录",false);
            }
            User user = (User)httpSession.getAttribute(Constant.USERINFO_SESSION_KEY);
            int userid = user.getId();
            int ret = loveMusicService.deleteLoveMusic(userid,musicId);
            if(ret == 1) {
                return new ResponseBodyMessage<>(0,"取消收藏成功!",true);
            }else {
                return new ResponseBodyMessage<>(0,"取消收藏失败!",false);
            }
        }

 10.对于删除音乐功能的完善

到这里,完成了音乐列表中的一些功能,也完成了收藏音乐列表中的一些功能,现在需要完善一下音乐的删除功能,因为音乐列表和收藏音乐列表之间是有一个逻辑的:

删除音乐列表的音乐时,收藏音乐列表的中的此音乐也会被一并删除掉

1.在LoveMusicMapper接口中新增代码

2.在LoveMusicMapper.xml中添加代码

//当删除库中的音乐的时候,同步删除lovemusic中的数据
    int deleteLoveMusicById(int musicId);
<!--    当删除库中的音乐的时候,同步删除lovemusic中的数据-->
    <delete id="deleteLoveMusicById" parameterType="java.lang.Integer">
    delete from lovemusic where music_id=#{musicId}
    </delete>

 在LoveMusicService类中新增代码 

public int deleteLoveMusicById(int musicId){
        return loveMusicMapper.deleteLoveMusicById(musicId);
    }

1.在MusicController类中的删除音乐方法中新增代码 

2.在MusicController类中注入LoverMusicService

注意:删除单个音乐和批量删除音乐中都要新增这个代码

//之所以要注入收藏音乐的mapper是因为删除部分,当删除库中的音乐的时候,同步删除lovemusic中的数据
    @Autowired
    private LoveMusicService loveMusicService;
//删除单个音乐
int deleteLoveMusicById(int iid);


//批量删除音乐
loveMusicService.deleteLoveMusicById(musicId);

前端页面和逻辑

前端页面:1.登录页面2.注册页面3.上传音乐页面4.音乐列表页面5.收藏音乐列表页面

前端逻辑:(注意:前端逻辑使用ajax,那么就需要导入jQuery)

1.登录页面逻辑

关于jQuery的参考:jQuery 教程

这里放一些这个项目里用到的操作:

1.$(function(){ });用于存放操作DOM对象的代码,执行其中代码时DOM对象已存在
2.$("#submit").click(function () {}); 当按钮点击事件被触发时会调用一个函数。
3.$.ajax({}); 参考:jQuery ajax - ajax() 方法
4.url:发送请求的地址
5.data:发送到服务器的数据。将自动转换为请求字符串格式
6.type:默认值: "GET")。请求方式 ("POST" 或 "GET"), 默认为 "GET"。注意:其它 HTTP 请求方法,如 PUT和 DELETE 也可以使用,但仅部分浏览器支持。
7.dataType:预期服务器返回的数据类型
8.success:请求成功后的回调函数。参数为:由服务器返回,并根据 dataType 参数进行处理后的数据;描述状态的字符串

1.登录页面和登录逻辑

<script>
		  // 核心业务逻辑
		  $(function(){
			  $("#submit").click(function(){
				  var username = $("#user").val();
				  var password = $("#password").val();

				  if(username.trim() == "" || password.trim() == "") {
					  alert("用户名或者密码不能为空!");
					  return;
				  }

				  $.ajax({
					  url:"/user/login",
					  data:{"username":username,"password":password},
					  type:"POST",
					  dataType:"json",
					  success:function(data) {
						  console.log(data);
						  if(data.status == 0) {
							  alert("登录成功!");
							  //跳转到指定的页面了
							  window.location.href="list.html";
						  }else{
							alert("登录失败,密码或者用户名错误!");
							$("#user").val("");
							$("#password").val("");
						  }
					  }
				  });
			  });
		  });
	  </script>

2.注册页面和逻辑 

注册页面和登录页面几乎一致,因此照搬登录页面就可以 

注册页面逻辑: 

// 核心业务逻辑
		  $(function(){
			  $("#submit").click(function(){
				  var username = $("#user").val();
				  var password = $("#password").val();

				  if(username.trim() == "" || password.trim() == "") {
					  alert("用户名或者密码不能为空!");
					  return;
				  }

				  $.ajax({
					  url:"/user/register",
					  data:{"username":username,"password":password},
					  type:"POST",
					  dataType:"json",
					  success:function(data) {
						  console.log(data);
						  if(data.status == 0) {
							  alert(data.message);
							  //跳转到指定的页面了
							  window.location.href="login.html";
						  }else{
							alert(data.message);
							$("#user").val("");
							$("#password").val("");
						  }
					  }
				  });
			  });
		  });

3.上传音乐页面和逻辑

注意:上传完音乐需要让页面跳转到音乐列表页面,因此此时我们需要在后端的MusicController中添加跳转页面的代码

resp.sendRedirect("/list.html");

4.音乐列表页面和逻辑

4.1页面

4.2逻辑

4.2.1展示所有音乐逻辑

$(function(){
            load();
        });



        //musicName可以传参  也可以不传参数
        function load(musicName) {

            $.ajax({

                url:"/music/findmusic",
                data:{"musicName": musicName},
                type:"GET",
                dataType:"json",
                success:function(obj) {
                    console.log(obj);
                    var data = obj.data;//数组

                    var s = '';
                    //data[i].id    data[i].singer  data[i].title
                    for(var i = 0; i < data.length;i++) {
                        var musicUrl = data[i].url+".mp3";
                        console.log(musicUrl);

                        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"  onclick="playerSong(\''+musicUrl+'\')"> 播放音乐 </button>' +'</td>';
                        s += '<td> <button class = "btn btn-primary"  onclick="deleteInfo('+data[i].id+')"> 删除 </button>' +
                        '<button class = "btn btn-primary"  onclick="loveInfo('+data[i].id+')"> 收藏 </button>' +'</td>';
                        s += "</tr>"
                    }
                    $("#info").html(s);
                }
            });
        }

4.2.2播放音乐逻辑

这里采用的是开源的播放控件,将开源项目下载到本地,取出player文件夹,放入static文件夹下

播放音乐这里的逻辑:

1.嵌入播放器

2.播放音乐逻辑

1.嵌入播放器 

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

 2.播放音乐逻辑

//播放音乐
        function playerSong(obj) {
            var name = obj.substring(obj.lastIndexOf("=")+1);
            //obj:播放的音乐的地址  name:播放的音乐的名称,0:播放的开始时间  false:不自动播放
            SewisePlayer.toPlay(obj,name,0,true);
        }

4.2.3删除指定音乐逻辑

//删除音乐
        function deleteInfo(obj) {
            console.log(obj);
            $.ajax({
                url:"/music/delete",
                type: "POST",
                data:{"id":obj},
                dataType:"json",

                success: function(val) {
                    console.log(val);

                    if(val.data == true) {
                        //删除成功!
                        alert("删除成功!,重新加载当前页面!");
                        window.location.href = "list.html";
                    }else{
                        alert("删除失败!");
                    }
                }
            });
        }

4.2.4查询+删除选中音乐逻辑

//查询音乐+删除选中的音乐
        $(function(){
            //查询音乐
            $("#submit1").click( function(){
                var name = $("#exampleInputName2").val();
                load(name);
            });




            //删除选中的音乐
            $.when(load).done(function(){
                $("#delete").click(function(){
                    var id = new Array();
                    var i = 0;//数组的下标
                    //
                    $("input:checkbox").each(function(){
                        //如果被选中,this代表发生事件的dom元素,<input>
                        if( $(this).is(":checked")) {
                            id[i] = $(this).attr("id");
                            i++;
                        }
                    });

                    console.log(id);

                    $.ajax({
                        url:"/music/deleteSel",
                        data:{"id":id},
                        dataType:"json",
                        type:"post",

                        success:function(obj){
                            if(obj.data == true) {
                                alert("删除成功!");
                                window.location.href = "list.html";
                            }else{
                                alert("删除失败!");
                            }
                        }
                    });
                });
            });
        });

4.2.5收藏音乐逻辑

//收藏音乐
        function loveInfo(obj) {
            $.ajax({
                url:"/lovemusic/likeMusic",
                type:"POST",
                data:{"id":obj},
                dataType:"json",

                success:function(val){
                    if(val.data == true) {
                        alert("收藏成功!");
                        window.location.href = "list.html";
                    }else{
                        alert("收藏失败!");
                    }
                }
            });
        }

5.收藏音乐列表页面和逻辑

页面和音乐列表基本一致,照搬音乐列表页面就可以

注意:这个页面也要嵌入播放器,照搬音乐列表页面就可以

逻辑:播放删除音乐 | 删除收藏音乐 | 查询收藏音乐

// 核心代码实现
    $(function(){
        load();

    });

    //musicName可以传参  也可以不传参数
    function load(musicName) {
        $.ajax({
            url:"/lovemusic/findlovemusic",
            data:{"musicName": musicName},
            type:"GET",
            dataType:"json",
            success:function(obj) {
                console.log(obj);
                var data = obj.data;//数组
                var s = '';
                //data[i].id    data[i].singer  data[i].title
                for(var i = 0; i < data.length;i++) {
                    var musicUrl = data[i].url+".mp3";
                    console.log(musicUrl);
                    s += '<tr>';
                    s += '<td>'+data[i].title+'</td>';
                    s += '<td>'+data[i].singer+'</td>';
                    s += "<td <a href=\"\">  <audio src= \""+ musicUrl+"\"  + controls=\"controls\" preload=\"none\" loop=\"loop\">  >"  + "</audio> </a> </td>";
                    //s += '<td> <button class = "btn btn-primary"  onclick="playerSong(\''+musicUrl+'\')"> 播放歌曲 </button>' +'</td>';
                    s += '<td> <button class = "btn btn-primary"  onclick="deleteInfo('+data[i].id+')"> 移除 </button>'  +'</td>';
                    s += "</tr>"
                }
                $("#info").html(s);
            }
        });
    }

    function playerSong(obj) {
        var name = obj.substring(obj.lastIndexOf("=")+1);
        //obj:播放的音乐的地址  name:播放的音乐的名称,0:播放的开始时间  false:不自动播放
        SewisePlayer.toPlay(obj,name,0,true);
    }

    function deleteInfo(obj) {
        console.log(obj);
        $.ajax({
            url:"/lovemusic/deletelovemusic",
            type: "POST",
            data:{"id":obj},
            dataType:"json",

            success: function(val) {
                console.log(val);

                if(val.data == true) {
                    //删除成功!
                    alert("删除成功!,重新加载当前页面!");
                    window.location.href = "list.html";
                }else{
                    alert("删除失败!");
                }
            }
        });
    }

    $(function(){
        $("#submit1").click( function(){
            var name = $("#exampleInputName2").val();
            load(name);
        });
    });

到此前端页面的代码与逻辑也完成了

最后收尾----配置拦截器

配置拦截器是为了防止除了注册和登录页面的其他页面在没有登录的时候能够访问

1.在com.example.anitamusicspace.config包中创建LoginInterceptor类

2.在com.example.anitamusicspace.config包中的AppConfig类中添加代码

1. LoginInterceptor类

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {
        HttpSession httpSession = request.getSession(false);
        if(httpSession!=null && httpSession.getAttribute(Constant.USERINFO_SESSION_KEY)!=null) {
            //此时是登录状态
            return true;
        }
        return false;
    }
}

2.AppConfig类

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //添加拦截器,将自定义拦截器加入到系统配置
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    //1、配置拦截规则
    LoginInterceptor loginInterceptor = new LoginInterceptor();
    registry.addInterceptor(loginInterceptor)
    .addPathPatterns("/**")
    //排除所有的JS
    .excludePathPatterns("/js/**.js")
    //排除images下所有的元素
    .excludePathPatterns("/images/**")
    .excludePathPatterns("/css/**.css")
    .excludePathPatterns("/fronts/**")
    .excludePathPatterns("/player/**")
    .excludePathPatterns("/login.html")
    .excludePathPatterns("/register.html")
    //排除登录接口
    .excludePathPatterns("/user/register")
    .excludePathPatterns("/user/login");

    }

    private void excludePathPatterns(String s) {
    }
}

到此,完结撒花

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

anita的音乐空间(项目) 的相关文章

随机推荐

  • Java Swing基础(顶层容器,中间层容器,原子组件)

    Swing基础 Swing顶层容器 Swing的3个顶层容器类 JFrame JApplet JDialog 都是重量级组件 分别继承了AWT组件Frame Applet和Dialog 每个顶层容器都有一个内容面板 通常直接或间接的容纳别的
  • 目前支持CUDA的nVIDIA的显卡型号 驱动及其 修改过后的 inf文件

    下载169 21 forceware winxp 32bit english whql exe NVIDIA Driver for Microsoft Windows XP with CUDA Support 169 21 我们在运行它的时
  • JDK8 网络Net包研究(二)

    完整的Socket 客户端 和 服务端实例代码 Client package lang socket import java io BufferedReader import java io IOException import java
  • 软件测试 git和gitee集成Pycharm 基于Flask的Mock Server服务器

    文章目录 1 Git 1 1 作用 1 2 工具 1 3 名称解释 2 安装git和注册Gitee 3 使用Git 1 clone克隆命令 2 初始化 3 查看文件状态 4 文件提交暂存区 5 提交到本地版本库 6 修改文件 7 查看日志
  • Google Cloud裁员 !为什么这么突然?

    点击上方 KotlinPython 关注 干货立马到手 大家好 我是gao 网易科技讯 2月15日消息 据国外媒体报道 作为内部机构重组举措的一部分 谷歌旗下云计算业务部门正在削减未指定数量的工作岗位 此举旨在提高该公司在云计算这个蓬勃发展
  • 基于实例的学习方法

    基于实例的学习方法 动机 基本概念 基于实例的学习 基于实例的概念表示 1 最近邻 最近邻的例子 理论结果 最近邻 1 NN 解释 问题 K 近邻 KNN KNN讨论1 距离度量 KNN 讨论2 属性 KNN 属性归一化 KNN 属性加权
  • Python 操作excel:新建文件并写入

    这里我们用到的库是 xlwt 代码实现 import xlwt 创建workbook和sheet对象 workbook xlwt Workbook 注意Workbook的开头W要大写 因为是个类对象 sheet1 workbook add
  • 关于在Spring配置文件中解决MySQL重连问题

    com alibaba druid pool DruidDataSource或org apache commons dbcp BasicDataSource连接池 自动重连配置 1 testWhileIdle配置
  • 蓝牙耳机连接笔记本电脑音量直接爆棚

    通常手机端和耳机端都有属于自己的音量 而绝对音量 AbsoluteVolume 就指的是蓝牙耳机和手机连接播放音频时双方音量同步 也就是当手机端音量调到最大时 耳机端的音量也是最大 声音加倍 快乐加倍有没有 绝对音量也称媒体音量同步 因此可
  • JavaWeb —— Servlet(看这篇就够了,通俗易懂快速掌握)

    前言 Serlet是什么 Servlet是服务端的小组件是一门动态获取页面资源的技术 是Java语言编写的一个类 Servlet运行在Web服务器中 他是由服务端调用以及执行的 学号Servlet是非常有必要的 Servlet是MVC的基石
  • 2022最新版Python安装教程,适合新手,赶快收藏!

    想要使用好Python这样一门解释性的语言 当然 掌握好安装方法也是极为重要的 安装不好Python 有可能会为你做开发或者在其他时候带来许多不必要的麻烦 接下来话不多说 直接开始 这里主要以Windows系统为例 Python的安装 打开
  • Vivado综合warning:[Synth 8-151] case item 5‘b10000 is unreachable

    问题 三段式状态机无法遍历所有状态 解决 1 检查reg nstate cstate的位宽是否足够容纳状态 2 检查FSM2中状态跳转逻辑是否有漏洞 3 检查是否设计了不需要的状态 本例中 参数列表如下 而状态寄存器位宽如下 即位宽不匹配导
  • gin框架源码分析——路由模块

    目录 一 什么是gin框架 二 gin初始化的过程 三 Engine中与路由相关的参数 1 路由相关参数的调用 2 重要参数详解 1 RouterGroup 2 trees 一 什么是gin框架 gin的官方简介如下 gin is a we
  • 美通社:2018年全球企业品牌影响力调查报告

    回顾2018年 全球各大公司大事不断 无论是正面新闻还是抨击报道 这些企业的影响力遍及全球 但是 大家好才是真的好 哪家企业才最受全球关注 有更大的影响力 上榜企业排名依次为 阿里巴巴 含蚂蚁金服 亚马逊 特斯拉 苹果 海航集团 通用电气
  • linux 下模拟网络延迟和丢包的工具tc的简单用法

    首先用ifconfig查看自己的网卡名字 为eno1 1 查看已经配置的网络条件 tc qdisc show dev eno1 2 删除网卡上面的相关配置 tc qdisc del dev eno1
  • 工商银行潍坊分行党建RPA机器人项目解析

    01 案例背景 银行业掀起引入RPA加速实现数字化转型的浪潮 近年来 金融科技的蓬勃发展极大促进了银行的业务创新 新技术 新业态层出不穷 随着银行业务和科技的融合逐步落实 银行业务正朝着线上化 智能化转变 科技赋能的转型范式将成为银行业的未
  • 做各列数据的简单统计图(纯代码)

    优化函数细节 def initial pic file path column name picture type import pandas as pd import matplotlib pyplot as plt 解决字体缺失导致最后
  • 连接计算机名提示输入网络凭据,Win10系统添加打印机提示输入网络凭据如何解决...

    当我们要使用打印机的时候 就需要在电脑中添加打印机才可以正常使用 可是有用户在升级到win10系统之后 要添加共享中的打印机的时候 却提示输入网络凭据输入你的凭据以连接到人事行政部 扫描的提示 该怎么办呢 针对这个问题小编就给大家讲解一下具
  • 软件提示vcruntime140_1.dll丢失的解决方法,以及丢失的原因总结

    在运行某些程序时 可能会出现 vcruntime140 1 dll 丢失 的错误提示 这是因为 vcruntime140 1 dll 是 Visual C Redistributable 的一部分 它通常被安装在 Windows 操作系统上
  • anita的音乐空间(项目)

    目录 项目核心功能 项目前置工作 1 创建项目 2 数据库设计 3 配置文件中配置数据库和xml 核心功能设计 1 登录功能 2 注册功能 3 上传音乐至音乐列表功能 4 播放音乐功能 5 删除音乐列表音乐功能 5 1删除音乐列表单个音乐功