项目实训第九周工作汇报

项目实训第九周工作汇报

一、工作内容

1、完成音乐播放器的前后端开发

2、完成用户自主导入解压音乐的前后端开发(AI生成音乐暂未完成)

3、完成用户音乐列表的前后端开发

二、界面展示

2.1我的音乐界面

在这里插入图片描述

2.2音乐列表界面

在这里插入图片描述

2.3睡眠主界面

在这里插入图片描述

三、细节讲解

3.1 用户导入音乐

实现逻辑:

1、用户从本地选择文件上传

2、后端收到文件后,将该文件拷贝复制到服务器的存储文件夹中

3、构建对应的Music对象,并将其插入数据库

4、上传且插入成功后,重新调用得到用户导入音乐列表并在前端完成更新

关键代码:

将后端收到的文件拷贝并存到服务器的存储文件夹:

if (file.isEmpty()) {
    System.err.println("上传的音乐文件为空");
    return Result.error("405","上传的文件为空");
}
String contentType = file.getContentType();
String originalFilename = file.getOriginalFilename();
if (!Objects.equals(contentType, "audio/mpeg") ||
        !originalFilename.toLowerCase().endsWith(".mp3")) {
    System.err.println("非法文件类型:" + contentType + " | " + originalFilename);
    return Result.error("408", "仅支持MP3格式");
}
long MAX_SIZE = 30 * 1024 * 1024;
if (file.getSize() > MAX_SIZE) {
    System.err.println("文件过大:" + file.getSize());
    return Result.error("413", "文件大小超过30MB限制");
}

对前端传递给后端的数据进行检查,文件为空报错,限制音频格式为MP3,限制文件大小最大为30MB。

synchronized (FileController.class) {
    flag = System.currentTimeMillis() + "";
    ThreadUtil.sleep(1L);
}

通过 System.currentTimeMillis() 串行加锁获取毫秒时间戳,生成唯一标识,并 sleep(1ms) 以避免并发重复。

String userDir = "user_" + userId;
String storagePath = filePath + "/music/" + userDir + "/";
if (!FileUtil.isDirectory(storagePath)) {
    FileUtil.mkdir(storagePath);
}
String encodedFileName = URLEncoder.encode(originalFilename, StandardCharsets.UTF_8.name());
String saveName = flag + "-" + encodedFileName;
FileUtil.writeBytes(file.getBytes(), storagePath + saveName);

user_{userId} 作为子目录进行分隔,先判断目录是否存在,不存在则创建。
对原始文件名做 URL 编码,前缀加上时间戳,确保文件名唯一且不含非法字符后写入磁盘。

String accessUrl = "http://" + ip + ":" + port + "/files/music/"
        + userDir + "/" + saveName;
Music music = new Music();
music.setMusicUrl(accessUrl);
music.setTitle(fileNameWithoutExtension);
musicService.addPersonMusic(fileNameWithoutExtension, accessUrl, userId, phyPath);
return Result.success(music);

拼接外部可访问的 HTTP URL,构建 Music 实体并通过 musicService 保存数据库,返回成功响应。

将music存入数据库:

public int addPersonMusic(String title, String musicUrl, Long userId, String storagePath)

传入的是音乐名称,音乐文件可访问url,用户的userId,音乐文件的实际存储路径。音乐的实际存储路径将用来计算音乐的时长。

long duration = 0;
String ffprobePath = "D:\\program files\\ffmpeg\\…\\bin\\ffprobe.exe";

由于引入ffprobe依赖失败,所以在本地下载了ffprobe,并配置了系统环境变量。

ProcessBuilder pb = new ProcessBuilder(
    ffprobePath,
    "-v", "error",
    "-show_entries", "format=duration",
    "-of", "default=noprint_wrappers=1:nokey=1",
    storagePath
);
pb.redirectErrorStream(true);

-show_entries format=duration:仅打印 “format” 部分的 duration 字段,计算时长。

-of default=noprint_wrappers=1:nokey=1:去掉字段名和包装,只输出纯数值。

storagePath:传入待解析文件的绝对路径。

redirectErrorStream(true):将 stderr 合并到 stdout,方便统一读取。

Process process = pb.start();
try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(process.getInputStream()))) {
    String line = reader.readLine();
    if (line != null) {
        double seconds = Double.parseDouble(line.trim());
        duration = (long) seconds;
        System.out.println("使用ffprobe获取时长成功:" + duration + " 秒");
    }
}
process.waitFor();

启动pb.start() 启动外部进程。

读取:通过 InputStreamReader 包装,使用 readLine() 读取首行(预期为浮点型秒数,如 123.456789)。

解析Double.parseDouble(...) 转为 double,然后截断(向下取整)赋值给 long duration

等待结束process.waitFor() 阻塞,直至子进程退出,确保资源释放。

由此解析出来了音频时长

Music music = new Music();
music.setUserId(userId);
music.setTitle(title);
music.setDuration(duration);
music.setMusicUrl(musicUrl);
music.setType(MusicTypeEnum.个人导入.name());
music.setPlayCount(0);
music.setIfFavorite(true);
music.setCreateTime(LocalDateTime.now());
return musicMapper.insert(music);

设置Music的其他属性,之后将其插入到music数据库。

3.2 将音乐导入播放列表

实现逻辑:

1、在我的音乐中选择一首音乐,点击“操作”中的“+”号,将音乐插入当前播放的列表当中。

2、重新获得当前的播放列表,在前端完成更新。

关键代码:
Long userId = BaseContext.getCurrentId();
PlayList nowPlayList = playListMapper.selectNowByUserId(userId);
Integer nowPlayListId = nowPlayList.getPlayListId();
List<PlayListMusic> playListMusics = playListMusicMapper.selectNowPlayListMusic(userId);
PlayListMusic playListMusic = playListMusicMapper.selectNowLocation(userId);

1、获取当前用户的userId。

2、从数据库获得当前的播放队列。

3、从数据库中获得当前队列中的所有歌曲,并获得当前歌曲中正在播放的歌的位置

具体的sql实现:

<!-- 查询当前PlayList -->
<select id="selectNowPlayListMusic" resultType="com.example.entity.PlayListMusic">
    SELECT
    plm.*
    FROM
    play_list_music AS plm
    JOIN (
    SELECT
    play_list_id
    FROM
    play_list
    WHERE
    user_id = #{userId}
    AND if_now = 1
    ) AS pl1
    ON plm.play_list_id = pl1.play_list_id
    ORDER BY
    plm.location ASC
</select>
if(playListMusic == null){
    playListMusic = new PlayListMusic();
    playListMusic.setPlayListId(nowPlayListId);
    playListMusic.setLocation(0);
    playListMusic.setId(null);
    playListMusic.setMusicUrl(music.getMusicUrl());
    playListMusic.setMusicId(music.getMusicId());
    playListMusic.setIfNow(false);
}else{
    Integer nowLocation = playListMusic.getLocation();
    for(PlayListMusic plm : playListMusics){
        if(plm.getLocation() > nowLocation){
            plm.setLocation(plm.getLocation() + 1);
            playListMusicMapper.updatePlayListMusics(plm);
        }
    }
    playListMusic.setLocation(nowLocation + 1);
    playListMusic.setId(null);
    playListMusic.setMusicUrl(music.getMusicUrl());
    playListMusic.setMusicId(music.getMusicId());
    playListMusic.setIfNow(false);
}
return playListMusicMapper.insertPlayListMusic(playListMusic);

默认将歌曲插入在当前播放歌曲的下一个位置,所以在当前播放歌曲之后的每一个歌曲的location+1并更新,最后插入新歌曲至播放列表。

3.3 实现音乐播放器

实现逻辑:

1、用户获得当前播放列表的歌曲,将歌曲按需播放。

2、实现暂停功能,点击暂停按钮,可以实现暂停。

3、实现进度条功能,可以记录歌曲播放到的位置,拖动进度条,可控制音乐的播放。

关键代码:

实现以上的功能更多是需要前端的代码

loadAudio(url)方法(根据url播放音频)

if (this.audioElement) {
  const eventsToRemove = {
    'ended': this.nextSong,
    'loadedmetadata': this.handleMetadata,
    'timeupdate': this.handleTimeUpdate,
    'error': this.handleAudioError
  };

  // 移除旧的事件监听
  Object.entries(eventsToRemove).forEach(([event, handler]) => {
    this.audioElement.removeEventListener(event, handler);
  });

  // 停止播放、清空 src 并卸载
  this.audioElement.pause();
  this.audioElement.removeAttribute('src');
  this.audioElement.load();
  this.audioElement = null;
}

判断旧实例:如果 this.audioElement 已经存在,说明此前有一个音频正在使用。

批量卸载事件监听:把所有之前注册的回调统一管理并逐一移除,避免内存泄漏和旧逻辑干扰。

彻底释放资源

  • pause():停止播放。
  • removeAttribute('src'):移除资源路径,切断与媒体文件的引用。
  • load():触发浏览器重新初始化 MediaElement,彻底清空内部状态。
  • 最后将 this.audioElement 置为 null,便于垃圾回收。
try {
  this.audioElement = new Audio(url);
  this.audioElement.preload = 'auto';
  this.audioElement.setAttribute('playsinline', '');
  // …
} catch (e) {
  console.error('音频初始化失败:', e);
  this.$emit('error', e);
}

构造函数:使用 new Audio(url) 动态创建一个 HTML5 音频元素。

预加载策略preload = 'auto' 表示浏览器可以提前加载媒体元数据和部分数据。

const eventHandlers = {
  'loadedmetadata': () => {
    this.duration = Math.floor(this.audioElement.duration);
    this.$emit('duration-update', this.duration);
  },
  'timeupdate': this.handleTimeUpdate,
  'ended': this.nextSong,
  'error': (e) => {
    console.error('音频错误:', e.target.error.code);
    this.$emit('error', e.target.error);
  },
  'stalled': () => this.isLoading = true,
  'canplaythrough': () => this.isLoading = false
};

Object.entries(eventHandlers).forEach(([event, handler]) => {
  this.audioElement.addEventListener(event, handler);
});

timeupdate:播放进度更新时触发,委托给 this.handleTimeUpdate 处理,更新进度条。

ended:播放结束,调用 this.nextSong 切换到下一首。

error:加载或播放出错时触发,打印错误码并通过事件上报。

handleTimeUpdate()方法

handleTimeUpdate() {
if (!this.isSeeking) {
  // ✅ 必须使用响应式更新
  this.$set(this, 'currentTime', Math.floor(this.audioElement.currentTime));

  // 调试用日志(确认事件触发)
  console.log('[进度更新]', this.currentTime, '/', this.duration); 
}
},

实时更新currentTime变量实现进度条更新。

handlePlayControl()方法

handlePlayControl() {
  // 1. 计算目标播放状态(播放 ⇄ 暂停)
  const targetState = !this.isPlaying;

  // 2. 如果还没有选中任何歌曲,默认从第一首开始播放
  if (typeof this.currentIndex === 'undefined') {
    this.currentIndex = 0;
    this.playThis(this.currentIndex);
    return;
  }

  // 3. 核心播放/暂停调用
  if (targetState) {
    this.resumePlayback();
  } else {
    this.pausePlayback();
  }

  // 4. 最后再更新 UI 状态标志
  this.isPlaying = targetState;
},

计算目标状态

  • targetState = !this.isPlaying:取反当前的播放布尔值,切换播放暂停状态。

首次播放处理

  • 如果 currentIndex 还未定义,说明从未播放过任何歌曲,直接播放第一首。

核心控制逻辑

  • 根据 targetState 调用不同方法:
    • resumePlayback() —— 恢复/开始播放。
    • pausePlayback() —— 暂停播放。

pausePlayback()方法

pausePlayback() {
  if (this.audioElement && !this.audioElement.paused) {
    try {
      this.audioElement.pause();
    } catch (e) {
      console.error('暂停失败:', e);
    }
  }
},

this.audioElement.pause()方法实现播放暂停。

resumePlayback()方法

resumePlayback() {
  if (this.audioElement) {
    const playPromise = this.audioElement.play();
    
    playPromise.catch(error => {
      console.warn('恢复播放失败:', error);
      this.showPlayButton = true;
    });
  } else {
    // 音频实例不存在时,重新加载并播放当前索引
    this.playThis(this.currentIndex);
  }
},

实例存在时

  • 调用 audioElement.play()继续播放。

实例缺失时

  • 如果 this.audioElementnull,说明播放器实例还没初始化,直接调用 playThis(currentIndex) 重新创建并播放当前歌曲。

四、下周任务规划

1、完成新用户的判定,并在新用户登陆后进入个性化信息收集界面。

2、完成个性化信息收集页面的前后端开发。

3、饮食打卡和运动打卡添加勋章判定逻辑,完成用户获得勋章的功能,并把用户获得的勋章通过勋章墙展示。

4、将勋章功能与负责区块链的同学对接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值