diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/back/FileController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/back/FileController.java index 7f0a177..ee29cc3 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/back/FileController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/back/FileController.java @@ -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,75 +36,95 @@ 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 { // 其他文件类型,保持下载行为 response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=" + java.net.URLEncoder.encode(fileName, "UTF-8")); } - + // 设置缓冲区大小和连接保持 response.setBufferSize(8192); // 8KB缓冲区 response.setHeader("Connection", "Keep-Alive"); - + try { // 使用流式下载而不是一次性加载全部内容到内存 boolean success = AliConfig.ossDownloadStream(ossPath, response.getOutputStream()); @@ -118,8 +139,8 @@ public class FileController extends BaseController { } } catch (IOException e) { // 检查是否为客户端断开连接的错误(Broken pipe) - if (e.getMessage() != null && - (e.getMessage().contains("Broken pipe") || + if (e.getMessage() != null && + (e.getMessage().contains("Broken pipe") || e.getMessage().contains("Connection reset by peer") || e.getMessage().contains("连接被对方重置") || e.getMessage().contains("你的主机中的软件中止了一个已建立的连接") || @@ -134,8 +155,8 @@ public class FileController extends BaseController { } catch (Exception e) { try { // 检查是否为客户端断开连接的错误 - if (e instanceof IOException && e.getMessage() != null && - (e.getMessage().contains("Broken pipe") || + if (e instanceof IOException && e.getMessage() != null && + (e.getMessage().contains("Broken pipe") || e.getMessage().contains("Connection reset by peer") || e.getMessage().contains("连接被对方重置") || e.getMessage().contains("你的主机中的软件中止了一个已建立的连接") || @@ -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; + } + } + } diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 825e7af..4e4b42f 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -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 # 数据库索引 @@ -147,4 +147,5 @@ umApp: IOSKey: 68a99a66ec2b5b6f8825b8b1 IOSSecret: tjbflqx0eoqjixtyrtbh0zgijawmvxfe # androidKey: 687b2df479267e0210b79b6f -# IOSKey: 687b2e1679267e0210b79b70 \ No newline at end of file +# IOSKey: 687b2e1679267e0210b79b70 +#Q1w2e3r4 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/config/AliConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/config/AliConfig.java index dcce5c5..960990d 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/config/AliConfig.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/config/AliConfig.java @@ -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(); + } + } + } + }