资讯详情

资讯详情

微信小程序+Java文件上传:MultipartFile落盘与静态访问

简介这份压缩包面向正在学习微信小程序与 Java 后端联调的开发者聚焦“小程序端选择文件并上传至服务器”这一常见需求提供前端与后端可对照的完整示例。代码按微信小程序前端和 Java 后台分层组织并给出两种上传路径一是将文件提交到 Java 本地服务器由 IndexController 接收处理二是上传至文件服务器NginxController 调用 service 与实体类完成流程UploadController 则直接编写上传逻辑便于比较分层实现与快速实现两种思路。压缩包共 23 个文件以 9 个 Java 源文件为核心辅以 json 配置、js 脚本、wxss 样式和 wxml 页面等小程序侧文件整体约 15KB体量轻便适合按模块阅读和二次改造。已有 2195 人学习能够帮助读者理解上传接口设计、前后端参数衔接及文件服务器接入方式也可作为课程设计或小型项目中文件上传模块的参考底稿。1. 从一次选文件上传说起这套 Java 文件服务到底能干什么微信小程序上传文件到文件服务器听起来像一句话真落地时却会在文件名、大小限制和访问路径上接连翻车。这份压缩包把两端放在一起一端是小程序选文件、看进度、拿回访问地址另一端是 Java 服务接收 multipart 请求、校验类型和大小、把文件落到磁盘再暴露静态访问路径。适合正在做后台附件、工单图片、课程资料上传的开发者也适合想拆一套最小上传链路、再换成对象存储的人。2. 工程骨架与技术选型小程序端、Java 服务端和文件落盘怎么分工拿到压缩包我一般先把两端拆开看小程序端只关心“选什么文件、传给谁、进度到哪、失败怎么提示”Java 服务端关心“请求是不是 multipart、文件空不空、类型允不允许、落到哪个目录、返回什么 URL”。这条边界划清楚后面改存储、加鉴权、换前端都不容易乱。2.1 目录结构先分清页面、接口和存储三层这类双端包常见组织方式大致如下具体文件名可能不同但职责基本跑不出这张表位置典型文件职责miniprogram/pages/upload/upload.wxml页面结构放选择按钮、进度条、结果提示miniprogram/pages/upload/upload.js页面逻辑调wx.chooseMessageFile、wx.uploadFile处理回调和进度server/.../FileUploadController.java接口层接收MultipartFile收业务参数返回统一结构server/.../FileStorageService.java存储层生成文件名、建目录、落盘、拼访问 URLserver/.../WebConfig.javaWeb 配置把上传目录映射成可访问的静态路径server/src/main/resources/application.yml运行配置端口、上传大小、上传目录、访问前缀我一般会把上传目录放在代码目录之外比如开发环境用./data/uploads部署环境用/data/uploads。这样打包、重启、清缓存都不会把用户文件误删。小程序端也不要直接拼服务端磁盘路径页面只认服务端返回的url或fileId否则后面换存储会牵一发动全身。2.2 技术栈组合Spring Boot MultipartFile 足够跑通最小闭环这个场景常见做法是 Java 8/11 Spring Boot 2.x前端用原生小程序不引 UI 框架也能跑。后端核心就是MultipartFileSpring MVC 收到multipart/form-data后把文件部分封装成MultipartFile业务代码只管调用transferTo或读流写盘。压缩包里的“文件服务器”如果是最小实现通常就是本地磁盘生产环境再换成独立文件服务或对象存储接口层不用大改。先在服务端目录启动cd server mvn spring-boot:run -Dspring-boot.run.arguments--server.port8080-Dspring-boot.run.arguments用来临时覆盖端口避免和本机其他服务撞车。启动后先看控制台有没有Tomcat started on port(s): 8080再用浏览器或 curl 打一个不存在的接口确认服务确实起来了。小程序端在开发阶段可以把请求地址指向本机但真机调试时手机访问不到电脑的127.0.0.1需要换成同一局域网内的电脑 IP 或测试环境 HTTPS 地址。2.3 上传链路一次点击经过哪些层一次上传的完整链路是页面按钮触发选择文件拿到临时路径tempFilePathwx.uploadFile把文件和formData组装成 multipart 请求Spring 的DispatcherServlet找到PostMapping(/upload)MultipartFile参数按name匹配FileStorageService校验、生成 UUID 文件名、按日期建目录、写盘控制器返回 JSON小程序解析res.data把服务端 URL 存到页面数据里。这条链路里最容易断的是两个点小程序name: file必须和服务端RequestParam(file)对上服务端返回的url必须和静态资源映射前缀对上。前者不对服务端收到空文件后者不对上传成功但访问 404。先把这两个字符串对齐再谈优化。提示上传目录不要用相对路径里的../拼也不要直接拿用户原始文件名当磁盘文件名。原始文件名只用于展示磁盘名用 UUID 更省心。3. 核心上传接口拆解一个 multipart 请求怎样被接收、校验、保存接口层我习惯先定返回结构再写控制器。小程序端只认code、message、data三个字段成功时data.url能直接访问失败时message能直接弹给用户。这样前端不用为每个接口写一套解析逻辑联调时也容易定位是网络错、参数错还是服务端异常。3.1 接口契约返回体先定前端才好写{ code: 0, message: ok, data: { fileId: 8f3c2b1a9d7e4c6f8a1b2c3d4e5f6a7b.png, originalName: 封面图.png, url: /files/cover/2025/03/12/8f3c2b1a9d7e4c6f8a1b2c3d4e5f6a7b.png, size: 182034, bizId: demo-1 } }code用0表示成功非0表示业务失败data.url是相对访问路径前端可以拼上服务端域名再展示fileId是磁盘上的真实文件名后面做删除、替换、审计时用得上originalName保留用户看得懂的名字方便下载时回显。注意url不要直接返回磁盘绝对路径否则既暴露目录结构也不方便换存储。3.2 控制器接住文件、业务类型和业务 IDpackage com.example.fileserver.controller; import com.example.fileserver.common.ApiResponse; import com.example.fileserver.service.FileStorageService; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.Map; RestController RequestMapping(/api/files) public class FileUploadController { private final FileStorageService storageService; public FileUploadController(FileStorageService storageService) { this.storageService storageService; } PostMapping(/upload) public ApiResponseMapString, Object upload( RequestParam(file) MultipartFile file, RequestParam(value bizType, defaultValue common) String bizType, RequestParam(value bizId, required false) String bizId) { // 空文件直接拦掉避免后面生成无意义的磁盘文件 if (file null || file.isEmpty()) { return ApiResponse.fail(40001, 文件为空); } MapString, Object data storageService.save(file, bizType, bizId); return ApiResponse.ok(data); } }RequestParam(file)里的file必须和小程序wx.uploadFile的name一致bizType用来分目录比如cover、attachment、avatar默认common防止前端漏传bizId是业务主键方便以后按业务反查文件。ApiResponse是项目里的统一返回包装类如果没有补一个带code、message、data的泛型类即可。控制器不直接碰Files.createDirectories存储细节留给服务层后面换对象存储时控制器不用改。3.3 存储服务UUID 文件名、日期目录和路径校验package com.example.fileserver.service; import com.example.fileserver.common.BizException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.*; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; Service public class FileStorageService { Value(${file.upload-dir}) private String uploadDir; Value(${file.access-prefix}) private String accessPrefix; private static final SetString ALLOWED_EXT new HashSet(Arrays.asList( jpg, jpeg, png, gif, webp, pdf, doc, docx, xls, xlsx, zip, txt )); public MapString, Object save(MultipartFile file, String bizType, String bizId) { if (file null || file.isEmpty()) { throw new BizException(40001, 文件为空); } String originalName StringUtils.cleanPath( Optional.ofNullable(file.getOriginalFilename()).orElse(unknown) ); if (originalName.contains(..)) { throw new BizException(40002, 文件名不合法); } String ext getExt(originalName); if (!ALLOWED_EXT.contains(ext)) { throw new BizException(40003, 不支持的文件类型); } String safeBizType bizType null || !bizType.matches([a-zA-Z0-9_-]{1,20}) ? common : bizType; String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); String storedName UUID.randomUUID().toString().replace(-, ) . ext; Path baseDir Paths.get(uploadDir).toAbsolutePath().normalize(); Path targetDir baseDir.resolve(safeBizType).resolve(datePath).normalize(); Path targetFile targetDir.resolve(storedName).normalize(); // 防止 bizType 或文件名把路径带出上传根目录 if (!targetFile.startsWith(baseDir)) { throw new BizException(40004, 保存路径越界); } try { Files.createDirectories(targetDir); file.transferTo(targetFile.toFile()); } catch (IOException | IllegalStateException e) { throw new BizException(50001, 文件保存失败); } String relativeUrl accessPrefix / safeBizType / datePath / storedName; MapString, Object result new LinkedHashMap(); result.put(fileId, storedName); result.put(originalName, originalName); result.put(url, relativeUrl); result.put(size, file.getSize()); result.put(bizId, bizId); return result; } private String getExt(String name) { int dot name.lastIndexOf(.); if (dot 0 || dot name.length() - 1) { return ; } return name.substring(dot 1).toLowerCase(Locale.ROOT); } }这段代码的关键点有四个cleanPath清掉路径里的杂字符contains(..)拦住明显的目录穿越ALLOWED_EXT做白名单别等上传了.jsp再补救UUID生成磁盘名避免同名覆盖和中文名乱码targetFile.startsWith(baseDir)做最后一道路径校验。bizType用正则限制成字母、数字、下划线和中划线防止有人传../../把目录带跑。file.transferTo写盘失败时统一转成业务异常后面由全局异常处理器返回50001前端能看到明确提示。3.4 静态映射与上传限制让文件存得下也取得到server: port: 8080 spring: servlet: multipart: max-file-size: 20MB max-request-size: 25MB file: upload-dir: ./data/uploads access-prefix: /filesmax-file-size是单个文件上限max-request-size是整个 multipart 请求上限后者要略大于前者因为请求里还有 boundary、表单字段和头部开销。upload-dir在开发环境可以用相对路径部署时建议改成绝对路径比如/data/uploadsaccess-prefix要和下面的静态映射保持一致否则返回的 URL 能看不能用。package com.example.fileserver.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.nio.file.Paths; Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String location Paths.get(uploadDir).toAbsolutePath().normalize().toUri().toString(); if (!location.endsWith(/)) { location location /; } registry.addResourceHandler(/files/**) .addResourceLocations(location); } }addResourceHandler(/files/**)对外暴露/files开头的路径addResourceLocations指向上传目录的绝对 URI。这里常见翻车是location少了结尾斜杠Spring 会把最后一段当文件名而不是目录另一个是上传目录用了相对路径启动目录一变映射就指到别处。把uploadDir转成绝对路径再拼 URI能少排很多 404。4. 小程序端上传实现选文件、组装 formData、处理进度和失败重试小程序端不用把逻辑写得太花核心就是三步选到文件、拿到临时路径、调上传接口。页面状态只保留percent、message、fileUrl三个字段用户能看见进度和结果就够了。真正容易出问题的是name对不上、超时太短、重复点击和返回体不是 JSON。4.1 页面结构一个按钮、一个进度条、一个结果区view classupload-box button typeprimary bindtapchooseAndUpload选择文件并上传/button progress percent{{percent}} show-info stroke-width6 / view classtip{{message}}/view view wx:if{{fileUrl}} classurl{{fileUrl}}/view /viewbindtap绑定选择事件progress的percent由上传进度回调更新show-info让用户看到百分比wx:if只在拿到 URL 后展示结果。页面不用把服务端返回的所有字段都铺出来fileUrl足够验证链路。样式类名可以按项目现有风格改结构不变。4.2 上传逻辑wx.uploadFile 的关键参数Page({ data: { percent: 0, message: 未上传, fileUrl: }, chooseAndUpload() { wx.chooseMessageFile({ count: 1, type: all, success: (res) { const file res.tempFiles[0] if (!file || !file.path) { wx.showToast({ title: 没有拿到文件, icon: none }) return } if (file.size 20 * 1024 * 1024) { wx.showToast({ title: 不能超过20MB, icon: none }) return } this.upload(file) }, fail: () { wx.showToast({ title: 选择文件失败, icon: none }) } }) }, upload(file) { this.setData({ percent: 0, message: 上传中 }) const task wx.uploadFile({ url: https://your-domain.com/api/files/upload, filePath: file.path, name: file, formData: { bizType: cover, bizId: demo-1 }, timeout: 30000, success: (res) { if (res.statusCode ! 200 p a hrefhttps://download.csdn.net/download/RitaNBBB/12838721 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →