音乐优化

This commit is contained in:
menxipeng
2025-08-29 14:12:39 +08:00
parent baa9bace5b
commit 26da694995
3 changed files with 316 additions and 56 deletions

View File

@@ -17,6 +17,7 @@ import com.ruoyi.system.service.IShareInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@@ -35,64 +36,84 @@ public class FileController extends BaseController {
* 下载OSS文件
*/
@GetMapping("/download/{objectName}/{fileName}")
public void downloadOssFile(@RequestParam(value = "musicId",required = false) String musicId, @PathVariable("objectName") String objectName, @PathVariable("fileName") String fileName, HttpServletResponse response) {
public void downloadOssFile(@RequestParam(value = "musicId",required = false) String musicId, @PathVariable("objectName") String objectName, @PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) {
try {
String ossPath = objectName + "/" + fileName;
// 检查用户登录状态
// 获取文件字节
// if (objectName.equals("musicFile")) {
// LoginUser userInfo = SecurityUtils.getLoginUser();
// if (userInfo == null){
// response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
// response.setContentType("application/json;charset=UTF-8");
// response.getWriter().write("{\"code\":401,\"msg\":\"用户未登录\"}");
// return;
// }
// SysUser sysUser = userInfo.getUser();
// if (sysUser == null) {
// if (StrUtil.isBlank(musicId)) {
// response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
// response.setContentType("application/json;charset=UTF-8");
// response.getWriter().write("{\"code\":400,\"msg\":\"音乐ID不能为空\"}");
// return;
// }
// ShopUser shopUser = userInfo.getShopUser();
// if (shopUser == null) {
// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// response.setContentType("application/json;charset=UTF-8");
// response.getWriter().write("{\"code\":401,\"msg\":\"用户未登录\"}");
// return;
// }
// //shopUser = shopUserMapper.selectShopUserByUserId(userId);
// MusicInfo musicInfo = musicInfoMapper.selectByMusicId(musicId);
//
// if (musicInfo == null) {
// response.setStatus(HttpServletResponse.SC_NOT_FOUND);
// response.setContentType("application/json;charset=UTF-8");
// response.getWriter().write("{\"code\":404,\"msg\":\"音乐信息不存在\"}");
// return;
// }
//
// if (musicInfo.getVip() != null && musicInfo.getVip() == 1) {
// // 判断用户vip
// if (!MusicUtil.getShopIsVip(shopUser)) {
// response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// response.setContentType("application/json;charset=UTF-8");
// response.getWriter().write("{\"code\":403,\"msg\":\"该音乐为VIP专享请升级VIP后下载\"}");
// return;
// }
// }
// }
// }
if (objectName.equals("musicFile")) {
LoginUser userInfo = SecurityUtils.getLoginUser();
if (userInfo == null){
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"用户未登录\"}");
return;
}
SysUser sysUser = userInfo.getUser();
if (sysUser == null) {
if (StrUtil.isBlank(musicId)) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":400,\"msg\":\"音乐ID不能为空\"}");
return;
}
ShopUser shopUser = userInfo.getShopUser();
if (shopUser == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"用户未登录\"}");
return;
}
//shopUser = shopUserMapper.selectShopUserByUserId(userId);
MusicInfo musicInfo = musicInfoMapper.selectByMusicId(musicId);
if (musicInfo == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"音乐信息不存在\"}");
return;
}
if (musicInfo.getVip() != null && musicInfo.getVip() == 1) {
// 判断用户vip
if (!MusicUtil.getShopIsVip(shopUser)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"msg\":\"该音乐为VIP专享请升级VIP后下载\"}");
return;
}
}
}
}
// 设置响应头
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
if (fileExtension.equals("mp3")) {
// 获取文件总长度这对iOS客户端获取时长非常重要
Long contentLength = getOssObjectLength(ossPath);
// 对于MP3文件设置为音频流支持直接播放
response.setContentType("audio/mpeg");
// 允许范围请求,支持断点续传
// 设置Content-Length头这对iOS客户端获取时长至关重要
if (contentLength != null) {
response.setHeader("Content-Length", contentLength.toString());
}
// 允许范围请求支持断点续传和iOS客户端获取时长
response.setHeader("Accept-Ranges", "bytes");
// 添加iOS兼容性头信息
response.setHeader("Cache-Control", "public, max-age=3600");
// 处理Range请求支持拖动播放和iOS客户端获取时长
String rangeHeader = request.getHeader("Range");
if (rangeHeader != null && rangeHeader.startsWith("bytes=") && contentLength != null) {
handleRangeRequest(rangeHeader, contentLength, ossPath, response);
return;
}
// 不设置Content-Disposition这样浏览器会直接播放而不是下载
} else {
// 其他文件类型,保持下载行为
@@ -159,4 +180,94 @@ public class FileController extends BaseController {
}
}
/**
* 获取OSS对象长度
*/
private Long getOssObjectLength(String ossPath) {
try {
return AliConfig.getOssObjectLength(ossPath);
} catch (Exception e) {
logger.warn("无法获取文件长度: {}", e.getMessage());
return null;
}
}
/**
* 处理Range请求
*/
private void handleRangeRequest(String rangeHeader, Long contentLength, String ossPath, HttpServletResponse response) throws IOException {
// 解析Range头
String[] ranges = rangeHeader.substring(6).split("-");
long start = Long.parseLong(ranges[0]);
long end = (ranges.length > 1 && !ranges[1].isEmpty())
? Long.parseLong(ranges[1])
: contentLength - 1;
// 确保范围有效
if (start >= contentLength) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
response.setHeader("Content-Range", "bytes */" + contentLength);
return;
}
if (end >= contentLength) {
end = contentLength - 1;
}
// 设置部分内容响应
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + contentLength);
response.setHeader("Content-Length", String.valueOf(end - start + 1));
// 使用范围下载
try {
boolean success = ossDownloadStreamRange(ossPath, response.getOutputStream(), start, end);
if (!success) {
handleFileNotFound(response);
}
} catch (IOException e) {
handleIOException(e);
}
}
/**
* OSS范围下载
*/
private boolean ossDownloadStreamRange(String ossPath, java.io.OutputStream outputStream, long start, long end) {
try {
return AliConfig.ossDownloadStreamRange(ossPath, outputStream, start, end);
} catch (Exception e) {
logger.error("范围下载失败: {}", e.getMessage());
return false;
}
}
/**
* 处理文件未找到
*/
private void handleFileNotFound(HttpServletResponse response) throws IOException {
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在或下载失败\"}");
}
}
/**
* 处理IO异常
*/
private void handleIOException(IOException e) throws IOException {
if (e.getMessage() != null &&
(e.getMessage().contains("Broken pipe") ||
e.getMessage().contains("Connection reset by peer") ||
e.getMessage().contains("连接被对方重置") ||
e.getMessage().contains("你的主机中的软件中止了一个已建立的连接") ||
e.getMessage().contains("Software caused connection abort"))) {
logger.info("客户端断开连接,文件传输中断: {}", e.getMessage());
} else {
throw e;
}
}
}

View File

@@ -16,7 +16,7 @@ ruoyi:
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
port: 8085
servlet:
# 应用的访问路径
context-path: /
@@ -68,7 +68,7 @@ spring:
# redis 配置
redis:
# 地址
host: 192.168.31.55
host: 127.0.0.1
# 端口默认为6379
port: 6379
# 数据库索引
@@ -148,3 +148,4 @@ umApp:
IOSSecret: tjbflqx0eoqjixtyrtbh0zgijawmvxfe
# androidKey: 687b2df479267e0210b79b6f
# IOSKey: 687b2e1679267e0210b79b70
#Q1w2e3r4

View File

@@ -256,4 +256,152 @@ public class AliConfig {
}
}
/**
* 获取OSS对象的长度
*
* @param objectName OSS对象名称
* @return 文件长度如果获取失败返回null
*/
public static Long getOssObjectLength(String objectName) {
// 配置参数
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
String accessKeyId = AliKeyConfig.ACCESS_KEY_ID;
String accessKeySecret = AliKeyConfig.ACCESS_KEY_SECRET;
String bucketName = "wenzhuangmusic";
OSS ossClient = null;
try {
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 判断文件是否存在
boolean exists = ossClient.doesObjectExist(bucketName, objectName);
if (!exists) {
log.error("OSS文件不存在: {}", objectName);
return null;
}
// 获取文件元数据
com.aliyun.oss.model.ObjectMetadata metadata = ossClient.getObjectMetadata(bucketName, objectName);
return metadata.getContentLength();
} catch (Exception e) {
log.error("获取OSS文件长度失败: {}", e.getMessage());
return null;
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 范围下载OSS文件支持HTTP Range请求
*
* @param objectName OSS对象名称
* @param outputStream 输出流
* @param start 开始字节位置
* @param end 结束字节位置
* @return 是否下载成功
*/
public static boolean ossDownloadStreamRange(String objectName, java.io.OutputStream outputStream, long start, long end) {
// 配置参数
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
String accessKeyId = AliKeyConfig.ACCESS_KEY_ID;
String accessKeySecret = AliKeyConfig.ACCESS_KEY_SECRET;
String bucketName = "wenzhuangmusic";
OSS ossClient = null;
InputStream inputStream = null;
try {
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 判断文件是否存在
boolean exists = ossClient.doesObjectExist(bucketName, objectName);
if (!exists) {
log.error("OSS文件不存在: {}", objectName);
return false;
}
// 创建范围请求
com.aliyun.oss.model.GetObjectRequest getObjectRequest = new com.aliyun.oss.model.GetObjectRequest(bucketName, objectName);
getObjectRequest.setRange(start, end);
// 获取文件对象
OSSObject ossObject = ossClient.getObject(getObjectRequest);
inputStream = ossObject.getObjectContent();
// 使用缓冲区进行流式传输
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
long totalBytesRead = 0;
long expectedBytes = end - start + 1;
while ((bytesRead = inputStream.read(buffer)) != -1 && totalBytesRead < expectedBytes) {
try {
// 确保不超过请求的字节范围
int bytesToWrite = (int) Math.min(bytesRead, expectedBytes - totalBytesRead);
outputStream.write(buffer, 0, bytesToWrite);
totalBytesRead += bytesToWrite;
// 定期刷新输出流
if (bytesToWrite == buffer.length) {
outputStream.flush();
}
} catch (java.io.IOException e) {
// 检查是否为客户端断开连接的错误
if (e.getMessage() != null &&
(e.getMessage().contains("Broken pipe") ||
e.getMessage().contains("Connection reset by peer") ||
e.getMessage().contains("连接被对方重置") ||
e.getMessage().contains("你的主机中的软件中止了一个已建立的连接") ||
e.getMessage().contains("Software caused connection abort") ||
e.getMessage().contains("SocketTimeoutException"))) {
// 客户端已断开连接,记录日志但不作为错误处理
log.info("客户端断开连接,文件传输中断: {}", e.getMessage());
return true; // 返回true因为这不是服务器端的错误
} else {
// 其他IO错误重新抛出
throw e;
}
}
}
outputStream.flush();
return true;
} catch (java.io.IOException e) {
// 检查是否为客户端断开连接的错误
if (e.getMessage() != null &&
(e.getMessage().contains("Broken pipe") ||
e.getMessage().contains("Connection reset by peer") ||
e.getMessage().contains("连接被对方重置") ||
e.getMessage().contains("你的主机中的软件中止了一个已建立的连接") ||
e.getMessage().contains("Software caused connection abort") ||
e.getMessage().contains("SocketTimeoutException"))) {
// 客户端已断开连接,记录日志但不作为错误处理
log.info("客户端断开连接,文件传输中断: {}", e.getMessage());
return true; // 返回true因为这不是服务器端的错误
} else {
// 其他IO错误
log.error("范围下载OSS文件失败: {}", e.getMessage());
return false;
}
} catch (Exception e) {
log.error("范围下载OSS文件失败: {}", e.getMessage());
return false;
} finally {
// 关闭资源
try {
if (inputStream != null) {
inputStream.close();
}
} catch (java.io.IOException e) {
log.error("关闭输入流失败: {}", e.getMessage());
}
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}