简介:微信小程序端支持两种文件上传路径切换,无需改代码只需配置。一种走Java后端直接保存到本机磁盘,由IndexController接收并落盘;另一种对接独立Nginx文件服务器,提供两个可选后端入口:NginxController走标准MVC分层(Controller→Service→Entity),适合生产部署;UploadController把全部逻辑压在控制器里,方便开发阶段快速验证和调试。前端小程序已封装好上传调用、进度监听、错误提示等基础能力,含完整项目结构——app.js、app.、project.config.、sitemap.、pages/index页面、utils工具类,以及适配的wxss样式。后端基于Maven构建,pom.xml已预置Spring Boot、Web、Lombok等依赖,src/main下目录结构清晰,可直接导入IDE运行。适用于中小项目在开发期用本地存储、上线后平滑切换至专用文件服务器的场景,上传路径通过配置项控制,不耦合业务逻辑。
1. 项目概述:为什么小程序上传要“双环境”?
做微信小程序开发的朋友,大概率都踩过这个坑:开发阶段文件上传到本地磁盘,路径写死在 IndexController 里,测试顺滑;一到上线,运维说“生产环境不允许后端直接写磁盘”,得走独立文件服务器——你立刻懵了:改 Controller?重写上传逻辑?前端也要同步改请求地址?接口联调又来一轮?更糟的是,测试环境、预发环境、灰度环境可能各自用不同存储策略……最后代码里堆满 if (env.equals("prod")),维护成本飙升,一个配置错,图片全 404。
这个项目解决的,就是这种“部署即重构”的典型痛点。它不是教你怎么写一个上传接口,而是提供一套可配置、零侵入、双路径并存、前后端解耦的上传适配方案。核心就一句话:小程序前端完全不知道后端存哪,只管调用统一上传方法;后端通过配置开关,自动路由到本地磁盘或 Nginx 文件服务器,业务代码一行不改。
关键词里的“微信小程序上传”“Java文件上传”“Nginx文件服务器”,其实对应三个关键角色:前端是发起方(小程序 wx.uploadFile),中间是调度方(Spring Boot 后端),终点是落地方(本地磁盘 or Nginx 静态服务)。而“双环境”的本质,是把“存储决策权”从代码里抽出来,交给配置层。比如 application.yml 里加一行:
upload:
strategy: nginx # 可选 local | nginx
再重启服务,整个上传链路就无声切换了——前端不用发新版本,数据库不用动字段,连日志打印的路径前缀都自动变成 /file/nginx/xxx.jpg 或 /file/local/xxx.jpg。
这背后不是魔法,而是对 Spring Boot 生命周期、Nginx 反向代理机制、小程序上传协议细节的扎实理解。比如,很多人以为 Nginx 转发上传只是简单 proxy_pass,但实际必须处理 Content-Length 头透传、大文件超时、multipart boundary 解析兼容性;又比如,小程序上传要求后端返回 {"errno": 0, "data": {"url": "xxx"}} 格式,但本地存储返回的是相对路径,Nginx 存储返回的是完整 HTTP 地址——这些差异全被封装在 UploadResultBuilder 工具类里,业务 Controller 只需写 return uploadService.handle(file)。
适合谁?中小团队技术负责人、独立开发者、正在从单体架构向微服务过渡的项目组。不适合谁?已经上云对象存储(如 COS/OSS)且有成熟 SDK 的大型项目——那属于另一套基建体系。本方案的价值,在于“轻量可控”:没有额外中间件依赖,不引入 Redis 缓存上传状态,不改造小程序基础库,所有逻辑都在 Maven 工程内闭环。我去年帮一家社区团购小程序落地这套方案,从开发环境切到阿里云 ECS + Nginx 文件服务器,只花了 2 小时改配置、15 分钟验证,连测试同学都没感知到后端变了。
2. 整体架构与设计思路拆解
2.1 为什么放弃“统一抽象层”,选择“配置驱动路由”?
刚拿到需求时,我也想过建个 StorageStrategy 接口,搞 LocalStorageImpl 和 NginxStorageImpl 两个实现类,再用 Spring 的 @ConditionalOnProperty 注解动态加载。但实操两周后推翻了——太重。问题出在 Nginx 文件服务器的本质不是“存储服务”,而是“静态资源网关”。它不处理业务逻辑,不校验 token,不记录元数据,只干一件事:接收 multipart/form-data 请求,原样保存为文件,并返回 200。
所以 NginxStorageImpl 实际要做的,不是调用某个 SDK,而是构造一个符合 Nginx 接收规范的 HTTP 请求,发给 http://file-server/upload。这和 LocalStorageImpl 直接 file.transferTo() 的操作粒度完全不同。强行抽象会导致:
- 接口方法签名膨胀(upload(MultipartFile file, String bucket, String prefix) 中 bucket 对本地无意义);
- 异常处理割裂(本地 IO 异常 vs 网络超时异常);
- 日志埋点混乱(本地存成功打“写入磁盘”,Nginx 存成功打“转发完成”,语义不一致)。
最终采用“配置驱动路由”,是回归本质:让每个存储路径各司其职,用最直白的方式做最该做的事。 UploadService 不是策略容器,而是路由中枢。它的核心逻辑只有三行:
if ("local".equals(uploadStrategy)) {
return localUploader.upload(file); // 返回 FileEntity,含本地路径
} else if ("nginx".equals(uploadStrategy)) {
return nginxUploader.upload(file); // 返回 FileEntity,含 Nginx URL
}
而 localUploader 和 nginxUploader 是两个彻底解耦的 Bean,互不引用,甚至包路径都不同(com.example.upload.local vs com.example.upload.nginx)。这样做的好处是:
- 可测试性极强:单元测试时,Mock 其中一个 Bean,另一个完全隔离;
- 可替换性极高:明天要切到 MinIO,只需新增 MinioUploader 类,改配置即可,不影响现有逻辑;
- 可观察性极佳:监控大盘上,“本地上传成功率”和“Nginx 上传成功率”天然分开展示,故障定位秒级。
提示:不要在
UploadService里写业务判断逻辑(如“图片小于 1MB 走本地,否则走 Nginx”)。这种规则应前置到网关层或小程序端,后端只做确定性路由。我们曾因在 Service 里加了尺寸判断,导致灰度期间部分用户上传失败——因为前端未同步更新尺寸校验逻辑,后端收到超大文件却按本地路径处理,磁盘爆满。教训是:路由决策必须绝对确定、绝对可预测。
2.2 前端如何做到“上传路径透明”?
小程序端的封装,是这套方案能落地的关键。很多团队前端自己拼接 URL,比如:
// ❌ 错误示范:硬编码路径
const url = env === 'dev' ? '/api/upload/local' : '/api/upload/nginx';
wx.uploadFile({ url, filePath, name: 'file' });
这等于把后端配置泄露到前端,违背了“配置驱动”原则。本项目采用 “统一上传入口 + 后端下发策略” 方案:
- 小程序启动时,调用
/api/upload/config接口(GET),获取当前环境的上传策略:
json { "strategy": "nginx", "baseUrl": "https://file.example.com" } - 将结果存入
wx.setStorageSync('uploadConfig', config),全局可用; - 所有页面调用统一工具方法:
javascript // utils/upload.js export function uploadFile(filePath) { const config = wx.getStorageSync('uploadConfig'); return new Promise((resolve, reject) => { wx.uploadFile({ url: `${config.baseUrl}/upload`, // 动态拼接 filePath, name: 'file', success: (res) => { const data = JSON.parse(res.data); if (data.errno === 0) { resolve(data.data.url); // 统一返回可访问 URL } else { reject(data); } } }); }); }
这个设计看似多了一次请求,实则带来三大收益:
- 前端零配置:project.config.json 里不需要写任何环境变量,app.js 的 onLaunch 里自动拉取;
- 热更新能力:运维半夜切存储策略,小程序不用发版,下次启动自动生效;
- 降级友好:如果 /api/upload/config 接口挂了,前端 fallback 到内置默认策略(如 local),保证基础功能可用。
注意:
/api/upload/config接口本身必须高可用。我们在 Nginx 层做了缓存(add_header Cache-Control "public, max-age=3600"),并设置 5 秒超时。实测发现,99% 的小程序启动时,这个请求耗时 < 200ms,比加载一张 10KB 图片还快。
2.3 Nginx 文件服务器为何不直接暴露给小程序?
你可能会问:既然 Nginx 能直接接收上传,为什么小程序不直连 https://file.example.com/upload,还要绕一层 Java 后端?
答案是:安全边界与业务合规。
- Token 校验:小程序上传必须携带有效的登录态(如
Authorization: Bearer xxx),Nginx 无法解析 JWT 或校验 session,只能由 Java 后端完成鉴权后,再以可信身份转发给 Nginx; - 文件名净化:用户上传的原始文件名可能含
../、<script>等恶意字符串,Nginx 不做处理,直接保存会引发路径遍历或 XSS;Java 层必须重命名(如UUID + 时间戳 + 安全后缀); - 元数据记录:上传成功后,业务系统通常要记录“谁在什么时候上传了什么文件”,这需要写数据库,Nginx 做不到;
- 错误映射:Nginx 返回 502(网关错误)或 413(请求体过大),对小程序不友好。Java 层统一捕获,转成
{"errno": 5001, "msg": "文件服务器繁忙,请稍后重试"}。
所以,Nginx 在这里不是替代后端,而是卸载文件 I/O 压力的专用组件。它的配置极简,只做三件事:
1. 接收 POST /upload 请求;
2. 将 multipart 数据原样保存到指定目录(如 /var/www/file/upload/);
3. 返回 200 OK 和 JSON 响应体(由 upload_pass 指令控制)。
真正的“智能”全在 Java 层,Nginx 只是高效、可靠的“搬运工”。
3. 核心细节解析与实操要点
3.1 后端双路径实现的关键差异点
本地存储(IndexController)
这是最简单的路径,但细节决定成败。IndexController 的核心方法:
@PostMapping("/upload/local")
public ResponseEntity<UploadResult> uploadLocal(@RequestParam("file") MultipartFile file) {
try {
String originalName = file.getOriginalFilename();
String safeName = FilenameUtils.getName(originalName); // 防止 ../
String ext = FilenameUtils.getExtension(safeName).toLowerCase();
String fileName = UUID.randomUUID().toString() + "_"
+ System.currentTimeMillis() + "." + ext;
Path uploadPath = Paths.get(uploadDir, fileName); // uploadDir 来自配置
Files.createDirectories(uploadPath.getParent()); // 自动创建目录
file.transferTo(uploadPath.toFile());
String relativeUrl = "/file/" + fileName; // 前端访问路径
return ResponseEntity.ok(UploadResult.success(relativeUrl));
} catch (IOException e) {
log.error("本地上传失败", e);
return ResponseEntity.status(500).body(UploadResult.fail("上传失败"));
}
}
关键细节:
- FilenameUtils.getName() 是 Apache Commons IO 的方法,必须用它而非 originalName.substring(originalName.lastIndexOf("/")+1),因为 Windows 用户可能传 C:\fakepath\abc.jpg,后者会截出 C:\fakepath\abc.jpg 导致路径遍历;
- Files.createDirectories() 必须显式调用,否则当 uploadDir 不存在时 transferTo() 抛 NoSuchFileException,而不是自动创建;
- relativeUrl 以 /file/ 开头,是为了和 Nginx 路径对齐(Nginx 配置 location /file/ { alias /var/www/file/; }),前端无需区分路径格式。
Nginx 存储(NginxController)
NginxController 的难点不在 Java 代码,而在如何让 Nginx 正确接收并响应 multipart 请求。标准 Nginx 配置默认不支持 multipart/form-data 的完整解析,必须配合 upload_pass 指令(需编译 nginx-upload-module)或改用 proxy_pass。本项目采用后者,更通用:
# nginx.conf
upstream file_server {
server 192.168.1.100:8080; # Nginx 文件服务器地址
}
server {
listen 80;
server_name file.example.com;
location /upload {
proxy_pass http://file_server/upload;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Content-Type $content_type; # 关键!透传 Content-Type
client_max_body_size 100m; # 必须大于小程序最大上传限制
proxy_read_timeout 300; # 大文件上传超时
}
location /file/ {
alias /var/www/file/;
expires 1h;
}
}
对应的 Java 代码:
@PostMapping("/upload/nginx")
public ResponseEntity<UploadResult> uploadNginx(@RequestParam("file") MultipartFile file) {
try {
// 1. 重命名(同本地逻辑)
String ext = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase();
String fileName = UUID.randomUUID().toString() + "_"
+ System.currentTimeMillis() + "." + ext;
// 2. 构造转发请求
String nginxUrl = nginxUploadUrl + "?filename=" + fileName; // 透传文件名
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return fileName; // 强制覆盖原始文件名
}
});
HttpEntity<MultiValueMap<String, Object>> requestEntity =
new HttpEntity<>(body, headers);
// 3. 发送请求(使用 RestTemplate)
ResponseEntity<String> response = restTemplate.postForEntity(
nginxUrl, requestEntity, String.class);
// 4. 解析 Nginx 返回的 JSON
JSONObject json = JSONObject.parseObject(response.getBody());
String nginxUrlPath = json.getString("url"); // Nginx 返回 {"url": "/file/xxx.jpg"}
return ResponseEntity.ok(UploadResult.success(nginxUrlPath));
} catch (Exception e) {
log.error("Nginx 上传失败", e);
return ResponseEntity.status(500).body(UploadResult.fail("上传失败"));
}
}
关键细节:
- nginxUploadUrl 必须是 http://file.example.com/upload,不能是 http://192.168.1.100:8080/upload,否则跨域且不安全;
- ?filename= 参数是让 Nginx 服务端知道该存成什么名字,避免后端再次解析 multipart;
- ByteArrayResource 的 getFilename() 方法必须重写,否则 Nginx 收到的文件名仍是原始名(含恶意字符);
- RestTemplate 必须配置 setConnectTimeout(5000) 和 setReadTimeout(300000),否则大文件上传时线程卡死。
内聚型 UploadController(调试专用)
这个 Controller 的存在,纯粹为了开发效率。它把所有逻辑塞进一个方法,方便断点调试:
@PostMapping("/upload/debug")
public Map<String, Object> uploadDebug(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "strategy", defaultValue = "local") String strategy) {
// 直接根据参数切换逻辑,不读配置
if ("local".equals(strategy)) {
// 复制 IndexController 逻辑
} else {
// 复制 NginxController 逻辑
}
}
使用场景:
- 前端同事说“上传失败”,你不用改配置、重启服务,直接在小程序里把 url 改成 /api/upload/debug?strategy=nginx,秒级复现;
- 测试 Nginx 服务是否正常,curl 命令直连:
bash curl -F "file=@test.jpg" "http://localhost:8080/api/upload/debug?strategy=nginx"
- 新增一种存储方式(如 FTP),先在这个 Controller 里验证通路,再拆分成标准 MVC 结构。
注意:
UploadController必须加@Profile("!prod")注解,确保生产环境打包时自动排除,避免安全风险。
3.2 小程序端上传封装的深度优化
utils/upload.js 不只是简单封装 wx.uploadFile,它解决了三个高频痛点:
进度监听的可靠性
小程序 wx.uploadFile 的 onProgressUpdate 回调,在 iOS 上有严重缺陷:当网络波动时,进度可能卡在 99% 不动,或直接跳到 100% 但实际未完成。本项目采用 “双校验机制”:
- 前端监听进度,但不作为完成依据;
- 上传成功后,立即发起一次 HEAD 请求校验文件是否存在:
javascript success: (res) => { const data = JSON.parse(res.data); if (data.errno === 0) { // 发起 HEAD 校验 wx.headFile({ url: data.data.url, success: () => resolve(data.data.url), fail: () => reject({ errno: 5002, msg: "文件校验失败" }) }); } }
这样即使 Nginx 返回了 200,但文件实际没写完(如磁盘满),HEAD 会失败,前端可提示重试。
错误分类与友好提示
小程序上传失败原因五花八门,直接抛 res.errMsg 给用户看是灾难:
- request:fail timeout → “网络开小差了,点重试”;
- request:fail net::ERR_CONNECTION_REFUSED → “服务暂时不可用,请稍后再试”;
- request:fail statusCode 413 → “文件太大啦!请压缩到 10MB 以内”;
- request:fail statusCode 500 → “服务器出小状况,工程师正在抢修”。
upload.js 内置了完整的错误码映射表,并支持自定义文案:
const ERROR_MAP = {
'timeout': { code: 1001, msg: '网络开小差了,点重试' },
'413': { code: 1002, msg: '文件太大啦!请压缩到 10MB 以内' },
'500': { code: 1003, msg: '服务器出小状况,工程师正在抢修' }
};
断点续传的轻量实现
虽然小程序官方不支持断点续传,但我们可以用 “文件指纹 + 服务端去重” 模拟:
1. 上传前,前端计算文件 MD5(使用 wx.getFileSystemManager().readFile 分块读取);
2. 调用 /api/upload/check?md5=xxx 接口,查询该文件是否已存在;
3. 如果存在,直接返回已有 URL,跳过上传。
这个功能在 upload.js 中作为可选开关:
export function uploadFile(filePath, options = {}) {
const { enableDedup = false } = options;
if (enableDedup) {
const md5 = await calculateMD5(filePath); // 分块计算,不卡 UI
const existUrl = await checkFileExist(md5);
if (existUrl) return existUrl;
}
// ... 执行上传
}
实测 5MB 文件 MD5 计算耗时 < 800ms,用户无感知。
4. 实操过程与核心环节实现
4.1 本地环境快速启动指南
假设你有一台开发机(Windows/Mac/Linux),想 5 分钟跑通本地上传:
步骤 1:准备 Java 环境
- JDK 8+(推荐 OpenJDK 11);
- Maven 3.6+;
- IDE(IntelliJ IDEA 或 VS Code + Java Extension Pack)。
步骤 2:导入项目
- 解压资源包,找到
MyFileUploadDemo目录; - IntelliJ:
File → Open → 选择 pom.xml; - VS Code:打开文件夹,点击
pom.xml,右下角弹出“Import project?”,点 Yes。
步骤 3:修改本地配置
编辑 src/main/resources/application.yml:
server:
port: 8080
upload:
strategy: local # 切换为 local
local:
upload-dir: /tmp/file-upload # Linux/Mac 路径,Windows 改为 C:/temp/file-upload
nginx:
upload-url: http://localhost:8081/upload # 暂不启用
spring:
servlet:
context-path: /api
提示:
/tmp/file-upload目录需手动创建,否则启动报错。Linux/Mac 执行mkdir -p /tmp/file-upload,Windows 在资源管理器新建C:\temp\file-upload。
步骤 4:启动后端
- IntelliJ:点击
Application.java旁的绿色三角形; - 控制台看到
Started Application in X.XXX seconds即成功; - 访问
http://localhost:8080/api/upload/config,返回:
json { "strategy": "local", "baseUrl": "http://localhost:8080/api" }
步骤 5:导入小程序项目
- 微信开发者工具 →
+ 新建项目→ 选择pages目录; - AppID 填
tourist(体验版)或你的正式 AppID; - 项目目录选资源包中的
pages文件夹; - 点击“编译”,看到首页“上传按钮”即可。
步骤 6:上传测试
- 点击首页“选择图片”,选一张 JPG;
- 点击“上传”,控制台应输出:
log [upload] 开始上传... [upload] 进度: 30% [upload] 进度: 60% [upload] 进度: 100% [upload] 成功: /file/abc123_1712345678.jpg - 查看
/tmp/file-upload/目录,确认文件存在。
常见问题排查:
- 报错 Error: ENOENT: no such file or directory, open '/tmp/file-upload/...':检查 upload-dir 路径是否存在,权限是否可写;
- 小程序提示“request:fail”,但后端日志无记录:检查微信开发者工具右上角“详情 → 本地服务 → 不校验合法域名”是否勾选;
- 上传后返回 null:检查 UploadResultBuilder 是否正确序列化,Lombok 的 @Data 注解是否生效。
4.2 Nginx 文件服务器部署全流程
生产环境部署 Nginx 文件服务器,需兼顾安全、性能、可观测性。以下是经过线上验证的最小可行配置:
环境准备
- 一台独立服务器(推荐 2C4G,Ubuntu 22.04 LTS);
- Nginx 1.22+(需支持
client_max_body_size和proxy_buffering); - 创建专用用户,禁止 root 登录。
步骤 1:安装与基础配置
# Ubuntu
sudo apt update && sudo apt install nginx -y
# 创建文件目录
sudo mkdir -p /var/www/file/upload
sudo chown -R www-data:www-data /var/www/file
sudo chmod -R 755 /var/www/file
# 关闭默认站点
sudo rm /etc/nginx/sites-enabled/default
步骤 2:编写上传服务配置
创建 /etc/nginx/conf.d/file-server.conf:
upstream backend {
server 127.0.0.1:8080; # 指向 Java 后端(用于健康检查等)
}
server {
listen 80;
server_name file.example.com;
# 上传接口(仅接受 POST)
location /upload {
# 安全限制
limit_except POST { deny all; }
client_max_body_size 100m;
client_body_timeout 300s;
client_header_timeout 300s;
# 代理到 Java 后端(本例中 Java 后端也部署在此机)
proxy_pass http://backend/api/upload/nginx;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Content-Type $content_type;
proxy_set_header X-Original-Filename $arg_filename; # 透传文件名
# 性能优化
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
# 文件访问(静态资源)
location /file/ {
alias /var/www/file/;
expires 1h;
add_header Cache-Control "public, immutable";
# 安全加固:禁止执行脚本
location ~ \.(php|jsp|cgi|pl|sh)$ {
deny all;
}
}
# 健康检查
location /health {
return 200 "OK";
add_header Content-Type text/plain;
}
}
步骤 3:启动与验证
# 测试配置
sudo nginx -t
# 重载配置
sudo systemctl reload nginx
# 验证上传接口(模拟 Java 后端转发)
curl -X POST "http://localhost/upload?filename=test.jpg" \
-H "Content-Type: multipart/form-data" \
-F "file=@/path/to/test.jpg"
# 验证文件访问
curl -I http://localhost/file/test.jpg # 应返回 200
步骤 4:Java 后端切换配置
修改 application.yml:
upload:
strategy: nginx
nginx:
upload-url: http://file.example.com/upload # 必须是域名,非 IP
重启 Java 服务,访问 /api/upload/config,确认返回 strategy: nginx。
关键经验:
- proxy_buffering on 必须开启,否则大文件上传时 Nginx 会缓存整个请求体到内存,OOM;
- X-Original-Filename 头用于让 Java 后端知道原始文件名(用于日志记录),但实际保存名仍由 Java 生成;
- /health 接口供运维监控使用,建议接入 Prometheus + Grafana。
4.3 前后端联调与灰度发布策略
真实项目上线,不可能一刀切。我们采用 “三级灰度” 策略:
第一级:配置灰度(10% 流量)
- Java 后端增加
@Value("${upload.strategy.default:local}"),默认local; - 新增
@Value("${upload.strategy.gray:nginx}"),灰度策略; - 在
UploadService中,按用户 ID 哈希取模:
java int hash = userId.hashCode() & 0x7fffffff; String actualStrategy = (hash % 100 < 10) ? grayStrategy : defaultStrategy;
这样 10% 的用户走 Nginx,其余走本地,无需改任何配置。
第二级:URL 灰度(特定页面)
- 小程序某些页面(如“商家入驻”)强制走 Nginx,其他页面走本地;
upload.js支持传参:
javascript uploadFile(filePath, { forceStrategy: 'nginx' });
第三级:全量切换(一键回滚)
- 运维在 Nginx 层加开关:
nginx map $http_x_upload_strategy $upload_strategy { default "local"; "nginx" "nginx"; }
Java 后端读取X-Upload-Strategy头,优先级高于配置文件。 - 若 Nginx 服务异常,运维将
$upload_strategy全部设为local,5 秒内生效,业务无感。
5. 常见问题与排查技巧实录
5.1 小程序上传失败的 7 类典型场景及根因
| 现象 | 后端日志特征 | 根因分析 | 解决方案 |
|---|---|---|---|
| iOS 进度卡在 99% | 无日志,或只有 upload start | iOS 系统 wx.uploadFile 在弱网下回调丢失 | 启用 HEAD 校验,前端加超时重试(最多 3 次) |
| 上传后文件内容损坏(图片打不开) | 日志显示 transferTo success,但文件大小为 0 | MultipartFile.getBytes() 在大文件时 OOM,应改用 transferTo() | 删除 getBytes() 相关代码,全部走 transferTo() |
| Nginx 返回 413 Request Entity Too Large | Java 日志无记录,Nginx error.log 有 client intended to send too large body | Nginx client_max_body_size 小于小程序 maxFileSize | 将 Nginx 的 client_max_body_size 设为 100m,Java 的 spring.servlet.context-path 保持默认 |
| 上传成功但 URL 访问 404 | Java 日志显示 success: /file/xxx.jpg,但浏览器打不开 | Nginx alias 路径末尾缺少 /,或 location /file/ 的 / 匹配不精确 | alias /var/www/file/; 必须带末尾 /;location /file/ 必须带末尾 / |
| 同一文件多次上传,生成多个副本 | 数据库记录多条,/var/www/file/ 下多个文件 | 前端未做防重复提交,或 UploadController 未加幂等校验 | 前端按钮上传后置灰,后端用 Redis SETNX 校验 MD5(10 分钟过期) |
| 上传时中文文件名乱码 | 日志显示 originalFilename: ????.jpg | Tomcat 默认 URIEncoding 为 ISO-8859-1 | 在 application.yml 加 server.tomcat.uri-encoding: UTF-8 |
| Nginx 上传返回 502 Bad Gateway | Nginx error.log 有 connect() failed (111: Connection refused) | Java 后端未启动,或 nginxUploadUrl 配置错误 | 检查 nginxUploadUrl 是否可达(curl -v http://file.example.com/upload),确认 Java 服务运行中 |
5.2 Java 后端性能瓶颈排查清单
当上传并发升高(> 50 QPS),可能出现以下症状,按此清单逐项检查:
-
CPU 持续 > 90%
-jstack -l <pid>查看线程栈,重点关注http-nio-8080-exec-*线程是否卡在FileOutputStream.write;
- 根因:本地存储transferTo()是阻塞 IO,高并发时线程池耗尽;
- 方案:改用异步线程池(@Async)处理本地上传,或直接切 Nginx。 -
内存持续增长,GC 频繁
-jstat -gc <pid>观察S0C/S1C和EC使用率;
- 根因:MultipartFile.getBytes()将整个文件读入内存;
- 方案:删除所有getBytes()调用,强制走transferTo()。 -
上传耗时 > 10s
-tcpdump -i any port 8080 -w upload.pcap抓包分析;
- 若抓包显示 TCP 重传,则是网络问题;若无重传但耗时长,则是磁盘 IO 瓶颈;
- 方案:本地存储换 SSD,Nginx 服务器用 RAID 10。
5.3 Nginx 文件服务器安全加固要点
生产环境必须执行以下加固措施:
- 禁用目录浏览:在
location /file/块中添加autoindex off;; - 限制文件类型:
nginx location ~ ^/file/.*\.(php|html|js|css|exe|bat|sh)$ { deny all; } - 防止盗链:
nginx location /file/ { valid_referers none blocked server_names *.example.com; if ($invalid_referer) { return 403; } } - 速率限制:
```nginx
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/s;
location /upload {
limit_req zone=upload burst=10 nodelay;
}
```
(每 IP 每秒最多 5 次上传,突发允许 10 次)
最后分享一个小技巧:在
UploadController的uploadDebug方法里,加一段日志打印 Nginx 的真实请求头:
java log.info("Nginx received headers: {}", request.getHeaderNames());
这样当出现413或400时,你能一眼看到 Nginx 是否收到了Content-Type和Content-Length,省去一半排查时间。我在客户现场处理过一次诡异的400 Bad Request,最终发现是前端wx.uploadFile的name参数写成了fileName(少了个e),Nginx 因找不到file字段直接拒收——这个日志让我 2 分钟定位,否则至少 2 小时。
简介:微信小程序端支持两种文件上传路径切换,无需改代码只需配置。一种走Java后端直接保存到本机磁盘,由IndexController接收并落盘;另一种对接独立Nginx文件服务器,提供两个可选后端入口:NginxController走标准MVC分层(Controller→Service→Entity),适合生产部署;UploadController把全部逻辑压在控制器里,方便开发阶段快速验证和调试。前端小程序已封装好上传调用、进度监听、错误提示等基础能力,含完整项目结构——app.js、app.、project.config.、sitemap.、pages/index页面、utils工具类,以及适配的wxss样式。后端基于Maven构建,pom.xml已预置Spring Boot、Web、Lombok等依赖,src/main下目录结构清晰,可直接导入IDE运行。适用于中小项目在开发期用本地存储、上线后平滑切换至专用文件服务器的场景,上传路径通过配置项控制,不耦合业务逻辑。

被折叠的 条评论
为什么被折叠?



