Spring Boot集成华为云OBS实现文件上传与预览功能(含安全下载)

Spring Boot集成华为云OBS实现文件上传与预览功能(含安全下载)

Spring Boot集成华为云OBS实现文件上传与预览功能(含安全下载)

本文基于Spring Boot + 华为云OBS(对象存储服务)实现一套完整的文件上传、预签名URL生成、安全预览/下载功能,并附带详细代码解析,适用于企业级应用中的文档管理、图片存储等场景。

一、背景与需求

在现代Web应用中,文件上传与访问是常见需求。出于安全性考虑,我们通常将文件存储在私有桶(Private Bucket)中,禁止直接公开访问。此时,需要通过预签名URL(Presigned URL) 机制临时授权用户访问特定文件。

本文将演示如何:

  • 配置华为云OBS连接
  • 限制上传文件类型
  • 将文件上传至OBS并记录元数据
  • 生成1小时有效的预签名URL用于前端预览或下载
  • 提供后端代理式文件预览接口(解决跨域、权限控制等问题)

二、项目结构概览

com.hrm.rmhr ├── config │ └── ObsConfig.java // OBS配置类 ├── controller │ └── FileController.java // 文件上传、预览、删除接口 ├── service │ ├── ObsService.java // OBS操作接口 │ └── impl/ObsServiceImpl.java// OBS服务实现 ├── entity │ └── FileMetadata.java // 文件元数据实体 └── mapper └── FileMetadataMapper.java // 数据库操作 

三、项目依赖(Maven)

<dependency><groupId>com.huaweicloud</groupId><artifactId>esdk-obs-java</artifactId><version>3.22.8</version><!-- 请使用最新版 --></dependency>

四、配置文件:application.yml

file:obs:endpoint: https://obs.cn-north-4.myhuaweicloud.com # 替换为你所在区域的Endpointak: YOUR_ACCESS_KEY_ID # 华为云AK(生产环境请用环境变量)sk: YOUR_SECRET_ACCESS_KEY # 华为云SKbucket-name: your-bucket-name # OBS桶名称storage-root-directory: /files/ # 存储根路径,结尾带斜杠

五、核心配置:ObsConfig

/** * OBS对象存储配置 */@Configuration@ConfigurationProperties(prefix ="file.obs")@DatapublicclassObsConfig{/** * OBS endpoint */privateString endpoint;/** * Access Key */privateString ak;/** * Secret Key */privateString sk;/** * Bucket名称 */privateString bucketName;/** * 存储根目录 */privateString storageRootDirectory;}
注意
🔐 安全提示:AK/SK属于敏感信息,建议通过配置中心或环境变量注入,避免硬编码。推荐使用:
ak: ${OBS_AK}sk: ${OBS_SK}

六、文件上传逻辑(FileController.uploadFile)

1. 安全校验

  • 检查文件是否为空
  • 校验文件扩展名(仅允许常见办公/图片格式)
privatestaticfinalSet<String>ALLOWED_EXTENSIONS=newHashSet<>(Arrays.asList("xls","xlsx","doc","docx","pdf","jpg","jpeg","png","gif","bmp","txt","csv"));

2. 调用OBS服务上传

  • 使用 MultipartFile 获取输入流
  • 构建唯一文件名(UUID + 原始名清洗)
  • 按年月+分类组织存储路径(如 resume/2025/12/xxx.pdf
  • 保存元数据到数据库(含原始名、大小、类型、上传人、租户等)

3. 返回预签名URL

上传成功后,立即生成一个1小时有效的预签名URL,供前端直接预览或下载:

{"code":200,"msg":"success","data":{"url":"https://bucket.obs.cn-north-4.myhuaweicloud.com/files/resume/2025/12/xxxx?Expires=...&AccessKeyId=...&Signature=...","fileId":"123"}}

五、预签名URL生成(generatePresignedUrl)

提供独立接口,支持按需生成不同有效期的访问链接:

@PostMapping("/generatePresignedUrl")publicResult<String>generatePresignedUrl(@RequestParam("fileId")Integer fileId,@RequestParam(value ="expirationSeconds", defaultValue ="3600")int expirationSeconds)
用途:前端在需要时动态获取新链接(如刷新过期链接),避免长期暴露URL。

六、安全预览接口(previewFile)

虽然前端可直接使用预签名URL访问文件,但存在以下问题:

  • 跨域问题(CORS)
  • 无法统一添加鉴权逻辑
  • 浏览器对某些Content-Type处理不一致(如PDF强制下载而非预览)

因此,我们提供后端代理式预览接口

@GetMapping("/preview/{fileId}")publicvoidpreviewFile(@PathVariableInteger fileId,HttpServletRequest request,HttpServletResponse response)throwsIOException

关键实现细节:

1. 设置正确的 Content-Type

从数据库读取 contentType(如 application/pdf),确保浏览器正确渲染。

2. 安全设置 Content-Disposition

使用 RFC 5987 标准 支持中文文件名,兼容新旧浏览器:

privateStringencodeFileName(String fileName){String asciiName = fileName.replaceAll("[^\\x20-\\x7e]","_");String utf8Encoded =URLEncoder.encode(fileName,StandardCharsets.UTF_8).replace("+","%20");return"inline; filename=\""+ asciiName +"\"; filename*=UTF-8''"+ utf8Encoded;}
inline 表示在浏览器中预览(非强制下载)filename* 支持UTF-8编码的文件名
3. 后端下载并转发

通过 new URL(presignedUrl).openStream() 从OBS拉取文件,写入 HttpServletResponse 输出流,实现“透明代理”。


七、文件删除(deleteFile)

  • 先删除OBS中的对象
  • 再标记数据库记录为已删除(软删除)
  • 避免因网络问题导致数据不一致

八、安全与最佳实践

  1. 不要暴露AK/SK:使用IAM角色或临时凭证更安全。
  2. 预签名URL有效期不宜过长:默认1小时,敏感文件可缩短至5~10分钟。
  3. 文件名清洗:防止路径穿越(如 ../../../etc/passwd)。
  4. 租户隔离:多租户系统中,storageRootDirectory 可包含 tenantCode
  5. 日志审计:记录上传/删除操作,便于追踪。

九、总结

本文完整实现了基于华为云OBS的文件上传与安全预览方案,兼顾功能性、安全性与用户体验。核心亮点包括:

✅ 严格的文件类型校验
✅ 按分类+时间组织存储结构
✅ 支持中文文件名的安全预览
✅ 预签名URL动态管理
✅ 后端代理解决跨域与权限问题

适用场景:HR系统简历上传、OA附件管理、医疗影像存储、教育资料分发等。

以下是完整、可直接运行的 Spring Boot 项目代码,包含:

  • ✅ MySQL 表结构适配(BIGINT idis_deleted TINYINT
  • ✅ MyBatis Mapper 接口 + XML
  • ✅ 华为云 OBS 上传/预览/软删除
  • ✅ Controller 支持上传与预览(从数据库查元数据)
  • ✅ 配置文件完整

📁 项目结构(Maven)

src/main/java/org/cskj/ ├── CskjApplication.java ├── config/ObsConfig.java ├── controller/FileController.java ├── entity/FileMetadata.java ├── mapper/FileMetadataMapper.java ├── service/ObsService.java └── service/impl/ObsServiceImpl.java src/main/resources/ ├── application.yml └── mapper/FileMetadataMapper.xml 

1️⃣ 主启动类:CskjApplication.java

packageorg.cskj;importorg.mybatis.spring.annotation.MapperScan;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication@MapperScan("org.cskj.mapper")publicclassCskjApplication{publicstaticvoidmain(String[] args){SpringApplication.run(CskjApplication.class, args);}}

2️⃣ OBS 配置类:ObsConfig.java

packageorg.cskj.config;importlombok.Data;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.stereotype.Component;@Component@ConfigurationProperties(prefix ="file.obs")@DatapublicclassObsConfig{privateString endpoint;privateString ak;privateString sk;privateString bucketName;privateString storageRootDirectory ="/files/";}

3️⃣ 实体类:FileMetadata.java

packageorg.cskj.entity;importlombok.Data;importjava.time.LocalDateTime;@DatapublicclassFileMetadata{privateLong id;privateString originalFilename;privateString safeFilename;privateString bucketName;privateLong fileSize;privateString contentType;privateString category;privateLocalDateTime uploadTime;privateString uploader;privateLong tenantCode;privateBoolean deleted;// is_deleted -> Boolean}

4️⃣ Mapper 接口:FileMetadataMapper.java

packageorg.cskj.mapper;importorg.apache.ibatis.annotations.Param;importorg.cskj.entity.FileMetadata;publicinterfaceFileMetadataMapper{intsave(FileMetadata record);FileMetadataselectById(@Param("id")Long id);voiddeletedFileMetadata(@Param("id")Long id);}
🔔 注意:id 类型为 Long,匹配 BIGINT

5️⃣ Mapper XML:FileMetadataMapper.xml

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="org.cskj.mapper.FileMetadataMapper"><resultMapid="FileMetadataResultMap"type="org.cskj.entity.FileMetadata"><idcolumn="id"property="id"/><resultcolumn="original_filename"property="originalFilename"/><resultcolumn="safe_filename"property="safeFilename"/><resultcolumn="bucket_name"property="bucketName"/><resultcolumn="file_size"property="fileSize"/><resultcolumn="content_type"property="contentType"/><resultcolumn="category"property="category"/><resultcolumn="upload_time"property="uploadTime"/><resultcolumn="uploader"property="uploader"/><resultcolumn="tenant_code"property="tenantCode"/><resultcolumn="is_deleted"property="deleted"javaType="boolean"jdbcType="TINYINT"/></resultMap><insertid="save"useGeneratedKeys="true"keyProperty="id"keyColumn="id"> INSERT INTO file_metadata ( original_filename, safe_filename, bucket_name, file_size, content_type, category, upload_time, uploader, tenant_code, is_deleted ) VALUES ( #{originalFilename}, #{safeFilename}, #{bucketName}, #{fileSize}, #{contentType}, #{category}, NOW(), #{uploader}, #{tenantCode}, 0 ) </insert><selectid="selectById"resultMap="FileMetadataResultMap"> SELECT id, original_filename, safe_filename, bucket_name, file_size, content_type, category, upload_time, uploader, tenant_code, is_deleted FROM file_metadata WHERE id = #{id} AND is_deleted = 0 </select><updateid="deletedFileMetadata"> UPDATE file_metadata SET is_deleted = 1 WHERE id = #{id} AND is_deleted = 0 </update></mapper>

6️⃣ Service 接口:ObsService.java

packageorg.cskj.service;importorg.cskj.entity.FileMetadata;importorg.springframework.web.multipart.MultipartFile;publicinterfaceObsService{FileMetadatauploadFile(MultipartFile file,String category,String uploader,Long tenantCode)throwsException;FileMetadatagetFileMetadata(Long fileId);StringgeneratePresignedUrl(Long fileId,int expirationSeconds)throwsException;booleandeleteFile(Long fileId);}

7️⃣ Service 实现:ObsServiceImpl.java

packageorg.cskj.service.impl;importcom.obs.services.ObsClient;importcom.obs.services.model.HttpMethodEnum;importcom.obs.services.model.PutObjectResult;importcom.obs.services.model.TemporarySignatureRequest;importcom.obs.services.model.TemporarySignatureResponse;importorg.cskj.config.ObsConfig;importorg.cskj.entity.FileMetadata;importorg.cskj.mapper.FileMetadataMapper;importorg.cskj.service.ObsService;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.web.multipart.MultipartFile;importjavax.annotation.PostConstruct;importjava.io.InputStream;importjava.time.LocalDate;@ServicepublicclassObsServiceImplimplementsObsService{@AutowiredprivateObsConfig obsConfig;@AutowiredprivateFileMetadataMapper fileMetadataMapper;privateObsClient obsClient;privatestaticfinalLogger log =LoggerFactory.getLogger(ObsServiceImpl.class);@PostConstructpublicvoidinit(){this.obsClient =newObsClient(obsConfig.getAk(), obsConfig.getSk(), obsConfig.getEndpoint());}@OverridepublicFileMetadatauploadFile(MultipartFile file,String category,String uploader,Long tenantCode)throwsException{String originalFilename = file.getOriginalFilename();if(originalFilename ==null) originalFilename ="unknown";String safeFilename =buildSafeFilename(category, originalFilename);String objectKey = obsConfig.getStorageRootDirectory()+ safeFilename;try(InputStream in = file.getInputStream()){PutObjectResult result = obsClient.putObject(obsConfig.getBucketName(), objectKey, in); log.info("OBS 上传成功: key={}, size={}, ETag={}", objectKey, file.getSize(), result.getEtag());}FileMetadata metadata =newFileMetadata(); metadata.setOriginalFilename(originalFilename); metadata.setSafeFilename(safeFilename); metadata.setBucketName(obsConfig.getBucketName()); metadata.setFileSize(file.getSize()); metadata.setContentType(file.getContentType()); metadata.setCategory(category); metadata.setUploader(uploader); metadata.setTenantCode(tenantCode); metadata.setDeleted(false); fileMetadataMapper.save(metadata);// ID 自动回填return metadata;}@OverridepublicFileMetadatagetFileMetadata(Long fileId){return fileMetadataMapper.selectById(fileId);}@OverridepublicStringgeneratePresignedUrl(Long fileId,int expirationSeconds)throwsException{FileMetadata meta =getFileMetadata(fileId);if(meta ==null){thrownewRuntimeException("文件不存在或已被删除");}String objectKey = obsConfig.getStorageRootDirectory()+ meta.getSafeFilename();TemporarySignatureRequest request =newTemporarySignatureRequest(HttpMethodEnum.GET, expirationSeconds); request.setBucketName(meta.getBucketName()); request.setObjectKey(objectKey);TemporarySignatureResponse response = obsClient.createTemporarySignature(request);return response.getSignedUrl();}@OverridepublicbooleandeleteFile(Long fileId){FileMetadata meta =getFileMetadata(fileId);if(meta ==null)returnfalse;try{String objectKey = obsConfig.getStorageRootDirectory()+ meta.getSafeFilename(); obsClient.deleteObject(obsConfig.getBucketName(), objectKey); log.info("OBS 文件删除成功: {}", objectKey);}catch(Exception e){ log.error("OBS 删除失败", e);returnfalse;} fileMetadataMapper.deletedFileMetadata(fileId);returntrue;}privateStringbuildSafeFilename(String category,String originalFilename){String cleanName = originalFilename.replaceAll("[^a-zA-Z0-9._\\-]","_");String timestamp =String.valueOf(System.currentTimeMillis());String filename = timestamp +"_"+ cleanName;String year =String.valueOf(LocalDate.now().getYear());String month =String.format("%02d",LocalDate.now().getMonthValue());if(category !=null&&!category.trim().isEmpty()){return category +"/"+ year +"/"+ month +"/"+ filename;}else{return year +"/"+ month +"/"+ filename;}}}

8️⃣ Controller:FileController.java

packageorg.cskj.controller;importorg.cskj.entity.FileMetadata;importorg.cskj.service.ObsService;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.*;importorg.springframework.web.multipart.MultipartFile;importjavax.servlet.http.HttpServletResponse;importjava.io.InputStream;importjava.net.URL;importjava.net.URLEncoder;importjava.nio.charset.StandardCharsets;/** * 文件管理控制器 * * 提供文件上传、预览、下载、删除等完整的文件管理功能。 * 通过集成 OBS(对象存储服务)实现文件的云端存储和管理, * 同时维护文件元数据信息,确保文件操作的安全性和可追溯性。 * * 主要功能包括: * 1. 文件上传:支持多种格式文件上传至OBS,并记录文件元数据 * 2. 文件预览(包含get和post两中方式):提供文件在线预览功能,支持多种文件格式 * 3. 预签名URL生成:生成临时访问URL,确保文件访问安全 * 4. 文件删除:安全删除OBS中的文件及对应的元数据 * 5. 文件类型验证:限制上传文件类型,确保系统安全 * * @author cskj * @date 2025/12/30 19:00 */@RestController@RequestMapping("/api/file")publicclassFileController{@AutowiredprivateObsService obsService;privatestaticfinalLogger log =LoggerFactory.getLogger(FileController.class);// 模拟用户信息(实际应从 Token 获取)privatestaticfinalStringDEFAULT_UPLOADER="system";privatestaticfinalLongDEFAULT_TENANT_CODE=1L;@PostMapping("/upload")publicResponseEntity<?>upload(@RequestParam("file")MultipartFile file,@RequestParam(value ="category", required =false)String category){if(file.isEmpty()){returnResponseEntity.badRequest().body("文件为空");}try{FileMetadata meta = obsService.uploadFile(file, category,DEFAULT_UPLOADER,DEFAULT_TENANT_CODE);String url = obsService.generatePresignedUrl(meta.getId(),3600);returnResponseEntity.ok().header("X-File-Id", meta.getId().toString()).body(url);}catch(Exception e){ log.error("上传失败", e);returnResponseEntity.status(500).body("上传失败: "+ e.getMessage());}}@GetMapping("/preview/{fileId}")publicvoidpreview(@PathVariableLong fileId,HttpServletResponse response){try{FileMetadata meta = obsService.getFileMetadata(fileId);if(meta ==null){ response.sendError(HttpServletResponse.SC_NOT_FOUND,"文件不存在");return;}String presignedUrl = obsService.generatePresignedUrl(fileId,3600);String contentType = meta.getContentType();if(contentType ==null|| contentType.isEmpty()){ contentType ="application/octet-stream";} response.setContentType(contentType);String encodedName =URLEncoder.encode(meta.getOriginalFilename(),StandardCharsets.UTF_8.toString()).replace("+","%20"); response.setHeader("Content-Disposition","inline; filename=\""+ meta.getOriginalFilename()+"\"; filename*=UTF-8''"+ encodedName);try(InputStream in =newURL(presignedUrl).openStream()){ in.transferTo(response.getOutputStream());}}catch(Exception e){ log.error("预览失败", e);try{ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"预览失败");}catch(Exception ignored){}}}/** * 文件预览接口(POST方式) * * 通过POST请求体传递文件ID,返回文件内容用于预览 * * @param requestBody 请求体,包含fileId参数 * @param response HTTP响应对象 * @throws IOException 文件读取异常 */@PostMapping("/preview")publicvoidpreviewFilePost(@RequestBodyMap<String,Integer> requestBody,HttpServletResponse response)throwsIOException{// 从请求体中获取fileIdInteger fileId = requestBody.get("fileId");if(fileId ==null){ response.sendError(HttpServletResponse.SC_BAD_REQUEST,"缺少文件ID");return;}String presignedUrl = obsService.generatePresignedGetUrl(fileId,3600);// 1小时有效FileMetadata metadata = fileMetadataService.selectById(fileId);if(metadata ==null|| metadata.getDeleted()){ response.sendError(HttpServletResponse.SC_NOT_FOUND,"文件不存在");return;} response.setContentType(metadata.getContentType());String contentDisposition =encodeFileName(metadata.getOriginalFilename()); response.setHeader("Content-Disposition", contentDisposition);try(InputStream in =newURL(presignedUrl).openStream();OutputStream out = response.getOutputStream()){byte[] buffer =newbyte[8192];int bytesRead;while((bytesRead = in.read(buffer))!=-1){ out.write(buffer,0, bytesRead);} out.flush();}catch(Exception e){ log.error("预览文件失败", e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"文件读取失败");}}@DeleteMapping("/{fileId}")publicResponseEntity<?>delete(@PathVariableLong fileId){boolean success = obsService.deleteFile(fileId);if(success){returnResponseEntity.ok("删除成功");}else{returnResponseEntity.status(404).body("文件不存在或已删除");}}}

9️⃣ 配置文件:application.yml

server:port:8080spring:datasource:url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghaiusername: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis:mapper-locations: classpath:mapper/*.xmlconfiguration:map-underscore-to-camel-case:truefile:obs:endpoint: https://obs.cn-north-4.myhuaweicloud.com # 替换为你的区域ak: YOUR_HUAWEI_CLOUD_ACCESS_KEY sk: YOUR_HUAWEI_CLOUD_SECRET_KEY bucket-name: your-bucket-name storage-root-directory: /files/ 

🔧 Maven 依赖(pom.xml)

确保包含以下关键依赖:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- Huawei OBS SDK --><dependency><groupId>com.huaweicloud</groupId><artifactId>esdk-obs-java</artifactId><version>3.22.8</version></dependency></dependencies>

✅ 如何运行

  1. 创建数据库并执行你的建表语句
  2. 修改 application.yml 中的数据库和 OBS 凭据

测试:

# 上传curl-F"[email protected]" http://localhost:8080/api/file/upload # 预览(替换 {id})curl http://localhost:8080/api/file/preview/1 

启动应用:

mvn spring-boot:run 

可直接复制运行
如有疑问,欢迎评论区交流!

提供一个前端验证的html文件

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>文件预览测试(POST 方式)</title><style>body{font-family: Arial, sans-serif;display: flex;justify-content: center;align-items: center;height: 100vh;margin: 0;background-color: #f5f7fa;}.container{text-align: center;padding: 30px;border: 1px solid #ddd;border-radius: 8px;background: white;box-shadow: 0 2px 10px rgba(0,0,0,0.1);width: 90%;max-width: 500px;}h2{color: #333;margin-bottom: 20px;}input[type="number"]{width: 100%;padding: 10px;margin: 10px 0;border: 1px solid #ccc;border-radius: 4px;box-sizing: border-box;font-size: 16px;}button{padding: 10px 20px;background-color: #409eff;color: white;border: none;border-radius: 4px;cursor: pointer;font-size: 16px;width: 100%;}button:hover{background-color: #53a8ff;}button:disabled{background-color: #ccc;cursor: not-allowed;}.tip{color: #666;font-size: 14px;margin-top: 15px;}.status{margin-top: 10px;font-size: 14px;color: #e6a23c;}</style></head><body><divclass="container"><h2>文件预览测试(POST 接口)</h2><p>请输入文件 ID(fileId):</p><inputtype="number"id="fileIdInput"placeholder="例如:69"min="1"/><br/><buttonid="previewBtn"onclick="previewFile()">🔍 预览文件</button><divid="status"class="status"></div><divclass="tip"> 💡 后端接口需为 POST /api/file/preview,接收 { "fileId": 69 },返回文件流(Content-Disposition: inline) </div></div><script>functionsetStatus(msg, isError =false){const statusEl = document.getElementById('status'); statusEl.textContent = msg; statusEl.style.color = isError ?'#f56565':'#e6a23c';}asyncfunctionpreviewFile(){const fileIdStr = document.getElementById('fileIdInput').value.trim();const btn = document.getElementById('previewBtn');if(!fileIdStr){alert('请输入文件ID');return;}const fileId =parseInt(fileIdStr,10);if(isNaN(fileId)|| fileId <=0){alert('请输入有效的正整数文件ID');return;}// 禁用按钮,防止重复点击 btn.disabled =true;setStatus('正在请求文件...');try{const response =awaitfetch('http://localhost:8080/app/api/file/preview',{method:'POST',headers:{'Content-Type':'application/json',// 如果有认证 token,记得加上:// 'Authorization': 'Bearer xxx'},body:JSON.stringify({fileId: fileId })});if(!response.ok){const errorMsg =await response.text().catch(()=>'未知错误');thrownewError(`HTTP ${response.status}: ${errorMsg}`);}// 获取文件的 MIME 类型(用于判断是否可预览)const contentType = response.headers.get('content-type')||'application/octet-stream';// 检查是否是可内联预览的类型(如 PDF、图片等)if(!contentType.startsWith('image/')&&!contentType.includes('pdf')&&!contentType.includes('text/')){if(!confirm('该文件类型可能无法在浏览器中直接预览,是否仍要下载?')){ btn.disabled =false;setStatus('');return;}}// 将响应转为 Blobconst blob =await response.blob();const url = window.URL.createObjectURL(blob);// 在新窗口打开预览const win = window.open(url,'_blank');if(!win){alert('浏览器阻止了弹出窗口,请允许后重试');}else{// 可选:监听窗口关闭后释放 URL(非必须)// win.addEventListener('beforeunload', () => window.URL.revokeObjectURL(url));}setStatus('预览已打开 ✅');}catch(error){ console.error('预览失败:', error);alert('预览失败:'+(error.message ||'网络或服务器错误'));setStatus('预览失败 ❌',true);}finally{ btn.disabled =false;// 3秒后清空状态setTimeout(()=>setStatus(''),3000);}}// 支持回车键触发 document.getElementById('fileIdInput').addEventListener('keypress',(e)=>{if(e.key ==='Enter'){previewFile();}});</script></body></html>

Read more

Effective Modern C++ 条款37:使std::thread在所有路径最后都不可结合

Effective Modern C++ 条款37:使std::thread在所有路径最后都不可结合

Effective Modern C++ 条款37:使std::thread在所有路径最后都不可结合 * 引言:线程生命周期的关键问题 * 线程的两种状态:可结合与不可结合 * 可结合(Joinable)状态的特征 * 不可结合(Unjoinable)状态的四种情况 * 为什么可结合性如此重要? * 两种被拒绝的替代方案 * RAII拯救方案:ThreadRAII类 * ThreadRAII实现详解 * 关键设计决策 * 实际应用案例 * 高级讨论:何时选择join或detach * 性能考量与最佳实践 * 结论:让线程管理无忧 BiliBili上对应的视频为:https://www.bilibili.com/video/BV1iZZgBiE9j 引言:线程生命周期的关键问题 在多线程程序设计中,std::thread的管理是一个看似简单实则暗藏玄机的话题。想象一下,你精心设计的并发程序在大多数情况下运行良好,却在某些边缘情况下突然崩溃——这正是许多开发者在使用原生线程时遇到的噩梦场景。本文将深入探讨std::thread对象

By Ne0inhk
RabbitMQ如何成为分布式系统的“神经中枢“?——从安装部署到C++调用实战的完整流程,带你体验它的奥妙所在!​

RabbitMQ如何成为分布式系统的“神经中枢“?——从安装部署到C++调用实战的完整流程,带你体验它的奥妙所在!​

文章目录 * 本篇摘要 * ①·RabbitMq(轻量级消息队列中间件) 介绍 * RabbitMQ 是什么? * 核心功能与特点 * 1. **核心功能** * 2. **核心优势** * RabbitMQ 的核心概念 * 1. **生产者(Producer)** * 2. **消费者(Consumer)** * 3. **队列(Queue)** * 4. **交换机(Exchange)** * 5. **绑定(Binding)** * 工作流程(以 Direct 交换机为例) * 常见应用场景 * RabbitMQ 与相关技术对比 * 图像理解 * 总结一句话 * ②·RabbitMq 安装教程 * RabbitMq安装 * **1. 安装 RabbitMQ** * **2. 启动 & 检查状态** * **3. 创建管理员用户(

By Ne0inhk
【STL】手撕 vector:从 0 到 1 模拟实现 STL 容器

【STL】手撕 vector:从 0 到 1 模拟实现 STL 容器

前言 STL 容器是 C++ 开发中绕不开的 “神兵利器”,而vector作为最常用的动态数组容器,更是新手入门 STL 的核心内容。但多数时候,我们只是 “会用”vector,却对它的底层逻辑一知半解 —— 比如它如何动态扩容?push_back的内存管理是怎样的?构造函数的匹配规则为何如此复杂? 与其停留在 “黑盒调用” 的层面,不如亲手模拟实现一个 vector:从底层的指针管理(_start/_finish/_endofstorage),到核心接口(push_back/resize/operator[]),再到构造、拷贝等特殊函数的实现,一步步揭开 STL 容器的面纱。 本文不会纠结过于晦涩的标准细节,而是以 “实用、易懂” 为核心,带你用 C++ 手动实现一个具备基础功能的vector—— 既能加深对容器原理的理解,也能锻炼 C++ 的底层编程能力。

By Ne0inhk
手把手实现 STL Set/Map:从零编写一棵红黑树到完整容器封装

手把手实现 STL Set/Map:从零编写一棵红黑树到完整容器封装

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 架构与实现:总览设计框架,深入源码细节 * 二. 核心设计思路:红黑树的泛型复用 * 2.1 红黑树的模板参数设计 * 2.2 仿函数 KeyOfT:统一 key 提取逻辑 * 2.3 核心约束:key 不可修改 * 三. 基础组件实现:红黑树与仿函数 * 3.1 红黑树节点结构 * 3.2 仿函数实现(map/set 层) * 3.2.1

By Ne0inhk