前言

  1. 关于上传文件方式
    官网提供: 获取上传的文件,提供了两种模式:File 模式Stream 流模式

  2. 上传文件之后,文件存储方式
    第一种存储方式: 存放在你自己的服务器上,和项目代码放在一起;
    第二种存储方式: 通过第三方OSS云存储服务,例如:七牛云、阿里云、腾讯云等它们的云存储服务,存放在第三方服务器上;

# 一、Stream 流模式上传文件(单个文件),文件存储在你自己的服务器上

# 1、安装依赖包和插件

npm i await-stream-ready stream-wormhole dayjs --save
// 安装了三个依赖或者插件
// await-stream-ready: 用于处理文件上传的流,当文件上传完毕后,会触发一个事件
// stream-wormhole: 用于处理文件上传的流,当文件上传完毕后,会触发一个事件
// dayjs: 用于处理时间插件

# 2、项目配置:config/config.default.js

//配置上传文件相关
config.multipart = {
    fileSize:'50mb', //最大上传大小50MB
    mode:'stream', //上传文件模式,stream 流模式 file 模式
    fileExtensions:['.xls','.txt','.jpg','.JPG','.png','.PNG',
                    '.gif','.GIF','.jpeg','.JPEG'],//文件格式自定义
};

# 3、新建控制器 app/controller/upload.js 处理文件上传,前后端都可以用

'use strict';

// 引入
const fs = require('fs');
const path = require('path');
//故名思意 异步二进制 写入流
const awaitWriteStream = require('await-stream-ready').write;
//管道读入一个虫洞
const sendToWormhole = require('stream-wormhole');
// 日期格式化
const dayjs = require('dayjs');

const Controller = require('egg').Controller;

class UploadController extends Controller {
 
    // 上传单个文件流模式到本地服务器
    async uploadStreamSingleToServer() {
        const fileDirname = 'uploads';
        // 单个文件,多个文件查看:https://www.eggjs.org/zh-CN/basics/controller#stream-%E6%A8%A1%E5%BC%8F
        const stream = await this.ctx.getFileStream(); //单个文件
        // 基础的目录:上传的文件存放的目录,如:'app/public/uploads'
        const uploadBasePath = 'app/public/' + fileDirname;
        // 生成唯一文件名:时间戳+随机数+文件后缀 [你也可以定义成其它的方式]
        const timestamp = Date.now(); //当前时间戳
        const random = Number.parseInt(Math.random() * 100000000); //随机数
        const extname = path.extname(stream.filename).toLocaleLowerCase(); //文件后缀
        const filename = `${timestamp}_${random}${extname}`;
        // 生成文件夹如:app/public/uploads后面的文件夹如:/2019/01/01,当然你可以自定义其他方式
        // const dirname = dayjs(Date.now()).format('YYYY/MM/DD');//使用我们上面安装的dayjs模块方法转换的日期格式
        const dirname = dayjs(Date.now()).format('YYYYMMDD');// /20190101
        //这个方法是判断这个文件夹是否存在,不存在则创建这个文件夹
        function mkdirsSync(dirname) {
            if(fs.existsSync(dirname)){
                return true
            } else {
                if(mkdirsSync(path.dirname(dirname))){
                    fs.mkdirSync(dirname)
                    return true
                }
            }
        }
        //调用方法生成完整文件路径
        mkdirsSync(path.join(uploadBasePath,dirname));
        // 生成写入路径: 完整文件路径 + 文件名
        const target = path.join(uploadBasePath,dirname,filename);
        // 写入流
        const writeStream = fs.createWriteStream(target);
        // 尝试写入文件数据流
        try {
            // 异步把文件流写入
            await awaitWriteStream(stream.pipe(writeStream));
        } catch (error) {
            // 出现错误,关闭管道
            await sendToWormhole(stream);
            this.ctx.throw(500,error);
        }
        //写入成功后返回文件路径地址
        let url = path.join('/public/' + fileDirname,dirname,filename).replace(/\\|\//g,'/');
        this.ctx.apiSuccess({ url });

    }

}

module.exports = UploadController;

# 4、配置路由 app/router.js

// 上传单个文件流模式到本地服务器
router.post('/uploadStreamSingleToServer', controller.upload.uploadStreamSingleToServer); 

# 5、如果不需要管理员登录就可以上传图片,配置中间件路由 config/config.default.js

// 对中间件adminAuth进一步配置
  config.adminAuth = {
    ignore: [
      ...,
      "/uploadStreamSingleToServer", //忽略文件上传路由需要管理员登录
    ],
  };

# 6、postman测试上传文件

  1. post请求,地址:http://127.0.0.1:7001/uploadStreamSingleToServer
  2. Body发送数据:form-data类型,Key值:files , Value:选择要上传的图片
  3. 返回结果示例:
{
    "msg": "ok",
    "data": {
        "url": "/public/uploads/20240309/1709952961932_9076609.jpg"
    }
}

我们上面这个方法基本已经能满足需求了,这里给大家扩展一下思路,就是如果你想自定义上传的文件路径,我们给大家扩展了下面这个方法,上传文件代码基本一样,就是对路径做了处理,因此这里就不讲代码了,感兴趣的同学,可以看一下这个方法,代码每一步注释写的也很清楚,具体代码如下:

# 7、扩展: 自定义上传文件路径

app/controller/upload.js

// 自定义上传路径上传单个文件流模式到本地服务器
async uploadStreamSingleToServerDiy() {
    //自定义文件都是放在uploads目录下的Diy文件夹下
    const fileDirname = 'uploads/Diy';
    // 基础的目录:上传的文件存放的目录,如:'app/public/uploads/Diy'
    let uploadBasePath = 'app/public/' + fileDirname;
    // 单个文件,多个文件查看:https://www.eggjs.org/zh-CN/basics/controller#stream-%E6%A8%A1%E5%BC%8F
    const stream = await this.ctx.getFileStream(); //单个文件
    
    // 根据传参得到用户自定义文件夹,多层文件夹用下划线传递参数,
    const { protocol, host } = this.ctx.request;
    let mydir = this.ctx.params.diydir;//console.log('接收的参数',mydir);
    //如果存在参数
    if(mydir){
        //将下划线转化为路径,如参数:mysourse_myfile,则转成 /mysourse/myfile文件夹
        // 一般给管理员或者用户使用,自定义参数一般是:管理员或者用户账号
        let mydirStr = '';
        let mydirArr = mydir.split('_');
        for(let i=0;i<mydirArr.length;i++){
            mydirStr += '/' + mydirArr[i];
        }
        mydir = mydirStr;
        //如果传参存在自定义文件夹,参数如:mysourse_myfile
        //则自定义文件夹路径如:app/public/uploads/Diy/mysourse/myfile
        uploadBasePath += mydir;
    }

    // 生成唯一文件名:时间戳+随机数+文件后缀 [你也可以定义成其它的方式]
    const timestamp = Date.now(); //当前时间戳
    const random = Number.parseInt(Math.random() * 100000000); //随机数
    const extname = path.extname(stream.filename).toLocaleLowerCase(); //文件后缀
    const filename = `${timestamp}_${random}${extname}`;
    // 生成文件夹如:app/public/uploads后面的文件夹如:/2019/01/01,当然你可以自定义其他方式
    // const dirname = dayjs(Date.now()).format('YYYY/MM/DD');//使用我们上面安装的dayjs模块方法转换的日期格式
    const dirname = dayjs(Date.now()).format('YYYYMMDD');// /20190101
    //这个方法是判断这个文件夹是否存在,不存在则创建这个文件夹
    function mkdirsSync(dirname) {
        if(fs.existsSync(dirname)){
            return true
        } else {
            if(mkdirsSync(path.dirname(dirname))){
                fs.mkdirSync(dirname)
                return true
            }
        }
    }
    //调用方法生成完整文件路径
    mkdirsSync(path.join(uploadBasePath,dirname));
    // 生成写入路径: 完整文件路径 + 文件名
    const target = path.join(uploadBasePath,dirname,filename);
    // 写入流
    const writeStream = fs.createWriteStream(target);
    // 尝试写入文件数据流
    try {
        // 异步把文件流写入
        await awaitWriteStream(stream.pipe(writeStream));
    } catch (error) {
        // 出现错误,关闭管道
        await sendToWormhole(stream);
        this.ctx.throw(500,error);
    }

    //写入成功后返回文件路径地址,注意访问路径开始不能是 app/
    //即 uploadBasePath变量如:app/public/uploads/Diy/mysourse/myfile
    //去掉app,返回:/public/uploads/Diy/mysourse/myfile
    let mydir_base = uploadBasePath.substring(3);
    let url = path.join(mydir_base, dirname, filename).replace(/\\|\//g, '/');
    // url = protocol + '://' + host + url; //如果想带上域名加上这句话
    this.ctx.apiSuccess({ url });
}

# 使用说明:

  1. 定义路由:app/router.js
  // 自定义上传路径上传单个文件流模式到本地服务器
  router.post('/uploadStreamSingleToServerDiy/:diydir', controller.upload.uploadStreamSingleToServerDiy); 
  1. 如果希望不登录也可以使用这个方法,同样需要再配置文件里面忽略掉权限验证 config/config.default.js
// 对中间件adminAuth进一步配置
  config.adminAuth = {
    ignore: [
      ...,
      "/uploadStreamSingleToServer", // 上传单个文件流模式到本地服务器
      "/uploadStreamSingleToServerDiy",// 自定义上传路径上传单个文件流模式到本地服务器
    ],
  };
  1. 这个方法我们规定的基本路径是:/public/uploads/Diy 文件夹下,去自定义文件夹;

  2. 这个方法虽然可以自定义参数生成文件夹,但是建议使用用户的账号作为参数,这样存储的文件就是以用户的账号分类的;

# 5. 具体使用方法:

一. 请求类型:POST
二. 请求路由地址方式一,自定义一个文件夹:http://127.0.0.1:7001/uploadStreamSingleToServerDiy/myuser01 参数建议用户或者管理员账号,返回的结果如下:

{
    "msg": "ok",
    "data": {
        "url": "/public/uploads/Diy/myuser01/20240310/1710038202074_99501763.png"
    }
}

三. 请求路由地址方式二,自定义多个文件夹进行嵌套:http://127.0.0.1:7001/uploadStreamSingleToServerDiy/mysourse_myfile 参数中多个文件夹名字必须用下划线隔开,返回的结果如下:

{
    "msg": "ok",
    "data": {
        "url": "/public/uploads/Diy/mysourse/myfile/20240310/1710038456814_52001040.png"
    }
}

# 二、File 模式

将在后面的课程:视频点播项目中使用

# 三、上传文件(图片)到阿里云存储OSS

# 1. 配置阿里云存储,拿到密钥和bucket等信息

来到阿里云控制台: 控制台 -> 对象存储 -> OSS

  1. 阿里云的云存储OSS默认是私有权限,创建完Bucket之后,可在Bucket列表-> 找到创建的Bucket -> 权限控制 -> 阻止公共访问【关闭】 -> 读写权限 -> 公共读写
  2. 概览-> 访问端口栏目 -> 配置Bucket信息
  3. 如何查看或设置accessIdaccessSecret
  1. 权限控制 -> 访问控制RAM -> 前往RAM控制台
  2. 身份管理 -> 用户组 -> 创建用户组

① 用户组名称: OSS
② 显示名称:OSS操作组
③ 备注:OSS操作组,用于测试文件上传、查看等

  1. 创建完成之后,进入刚刚创建的用户组

权限管理 -> 新增授权-> 搜索 oss -> 将 oss相关权限勾上 AliyunOSSFullAccess,AliyunOSSReadOnlyAccess
身份管理 -> 用户 -> 创建用户

用户账号信息:

  1. 登录名称: thinkphp_eggjs
  2. 显示名称: thinkphp_eggjs两套框架上传图片等文件
    访问方式:
  3. 选择用 使用永久 AccessKey 访问创建 AccessKey ID 和 AccessKey Secret,支持通过 API 或其他开发工具访问
    创建成功后,可复制 AccessKey IDAccessKey Secret,用于配置文件系统
  1. 选中创建的用户 -> 添加到用户组 -> OSS操作组 -> 确定

# 2. 兼容多版本eggjs,安装上传的插件命令

安装:

// 安装`egg-multipart`插件,ctx.multipart() 方法由该插件提供: 
// egg-multipart@2 # 指定兼容版本
npm install egg-multipart@2 --save
// 安装阿里云OSS SDK命名:
npm install ali-oss --save
// 安装命名:
npm install ali-oss moment stream-wormhole --save

启用插件 (config/plugin.js): 启用 egg-multipart 插件:ctx.multipart() 方法由该插件提供

multipart: {
    enable: true,
    package: 'egg-multipart'
},

引入:

// 阿里云OSS SDK
const OSS = require('ali-oss');
// node系统模块
const path = require('path');
const fs = require('fs');
// 上传插件
const sendToWormhole = require('stream-wormhole');
const moment = require('moment');

# 四、上传文件(图片)到阿里云存储OSS--File 模式(完整流程和代码)

# 1. 环境说明

运行环境最低版本为以下的版本。

`node -v`:v18.12.1  `node版本` 
`npm list egg`:
                egg-mock@5.10.9
                │ └── egg@3.17.5 deduped   
                └── egg@3.17.5  `egg版本`           
`npm -v`8.19.2  `npm版本` 

# 2. 安装阿里云OSS SDK命令

npm install ali-oss --save

# 3. 路由

app/router/admin/shop.js

// 图片上传阿里云
router.post('/shop/admin/image/uploadAliyun',controller.admin.image.uploadAliyunOSS);

# 4.配置:config/config.default.js

...
  //配置上传文件相关
  config.multipart = {
    fileSize: '50mb', //最大上传大小50MB
    mode: 'file', //上传文件模式,stream 流模式 file 模式
    fileExtensions: ['.xls', '.txt', '.jpg', '.JPG', '.png', '.PNG',
      '.gif', '.GIF', '.jpeg', '.JPEG', 
      '.doc', '.docx', '.pdf', '.PDF', '.xlsx', '.XLSX'],//文件格式自定义
    cleanSchedule: {
      cron: '0 30 4 * * *', // 每天凌晨4:30清理临时文件
    },
    
  };

  // 上传文件到阿里云oss的配置
  config.oss = {
    client: {
      accessKeyId: '你自己的阿里云oss的accessKeyId',
      accessKeySecret: '你自己的阿里云oss的accessKeySecret',
      bucket: 'thinkphp-eggjs', //你的bucket名称
      endpoint: 'oss-cn-hangzhou.aliyuncs.com', //你的bucket所在地域
      timeout: '60s', //上传文件超时时间
    }
  };
...

# 5. 扩展方法

app/extend/context.js

'use strict';

// 阿里云OSS SDK
const OSS = require('ali-oss');
// node系统模块
const path = require('path');
const fs = require('fs/promises');
const crypto = require('crypto');

module.exports = {
    ...
    // 通用文件上传到阿里云OSS方法--File模式
    /**
     * 通用文件上传到阿里云OSS方法--File模式
     * @param {string} fieldName - 上传文件的字段名
     * @param {number} imageClassId - 图片分类ID,默认为0
     * @param {string} prefix - 阿里云oss的Bucket中最外层文件夹名称,默认为'images'
     * @returns {Array} - 上传结果数组,每个元素对象包含上传文件的URL、路径、分类ID和创建时间
     */
    async uploadOSS_File(fieldName, imageClassId = 0, prefix = 'images') {
        const { app } = this;
        
        try {
            // 兼容 Egg 3.x 的文件结构
            if (!this.request.files || this.request.files.length === 0) {
            throw new Error('请选择要上传的文件');
            }

            // 筛选指定字段的文件
            const matchedFiles = this.request.files.filter(
            f => f.fieldname === fieldName
            );

            if (matchedFiles.length === 0) {
            throw new Error(`未找到 ${fieldName} 字段的上传文件`);
            }

            // 统一处理为数组
            const fileList = matchedFiles;

            // 创建 OSS 客户端
            const client = new OSS(app.config.oss.client);
            
            // 并行上传处理
            const results = await Promise.all(
            fileList.map(async file => {
                try {
                // 生成唯一文件名
                const timestamp = Date.now();
                const randomStr = crypto.randomBytes(6).toString('hex');
                const extname = path.extname(file.filename).toLowerCase();
                const filename = `${timestamp}_${randomStr}${extname}`;

                // 创建日期路径
                const now = new Date();
                const datePath = [
                    now.getFullYear(),
                    String(now.getMonth() + 1).padStart(2, '0'),
                    String(now.getDate()).padStart(2, '0')
                ].join('');

                // 构造完整路径
                const ossPath = `${prefix}/${datePath}/${filename}`;

                // 读取文件内容
                const fileContent = await fs.readFile(file.filepath);

                // 上传到 OSS
                const ossRes = await client.put(ossPath, fileContent);

                return {
                    url: ossRes.url,
                    path: ossPath,
                    image_class_id: Number(imageClassId),
                    create_time: Math.floor(Date.now() / 1000)
                };
                } finally {
                // 清理临时文件
                await fs.unlink(file.filepath).catch(() => {});
                }
            })
            );

            return results;
        } catch (e) {
            // 全局异常清理
            if (this.request.files) {
            await this.cleanupRequestFiles();
            }
            throw e;
        }
    },
};

# 6. 控制器

app/controller/admin/image.js

...
class ImageController extends Controller {
    // 通用文件上传到阿里云OSS方法
    async uploadAliyunOSS() {
        const { ctx,app } = this;
        try {
            
            // 获取分类ID(通过字段传递)
            const imageClassId = ctx.request.body.image_class_id || 0;

            // 通用文件上传到阿里云OSS方法--File模式
            const result = await ctx.uploadOSS_File('img', imageClassId, 'images');

            ctx.body = {
                code: 200,
                msg: '上传成功',
                data: result
            };
            
        } catch (error) {
            ctx.status = 500;
            ctx.body = {
                code: 500,
                msg: error.message,
                data: []
            };
        }
    }
}
...

# 7.postman测试方法

# 1. 单图上传测试:

方法:POST
URL:http://127.0.0.1:7001/shop/admin/image/uploadAliyun
Body选择form-data格式:
添加字段:
Key: image_class_id,Value: 0类型Text
Key: img,Value: 选择文件(类型File

# 2. 多图上传测试:

方法:POST
URL:http://127.0.0.1:7001/shop/admin/image/uploadAliyun
Body选择form-data格式:
添加字段:
Key: image_class_id,Value: 0类型Text
Key: img,Value: 选择多个文件(点击字段右侧下拉箭头选择类型File后,可多选文件)
或者 填写多个img字段,每个字段选择一个文件(类型File

# 8. 返回实例

// 单图
{
    "code": 200,
    "msg": "上传成功",
    "data": [
        {
            "url": "http://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250417/1744866732481_20f90b51544f.png",
            "path": "images/20250417/1744866732481_20f90b51544f.png",
            "image_class_id": 0,
            "create_time": 1744866732
        }
    ]
}
// 多图
{
    "code": 200,
    "msg": "上传成功",
    "data": [
        {
            "url": "http://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250417/1744866760867_f376ca377d65.png",
            "path": "images/20250417/1744866760867_f376ca377d65.png",
            "image_class_id": 0,
            "create_time": 1744866761
        },
        {
            "url": "http://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250417/1744866760867_85681992b9b1.png",
            "path": "images/20250417/1744866760867_85681992b9b1.png",
            "image_class_id": 0,
            "create_time": 1744866761
        },
        {
            "url": "http://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250417/1744866760867_5aab3128bc21.png",
            "path": "images/20250417/1744866760867_5aab3128bc21.png",
            "image_class_id": 0,
            "create_time": 1744866761
        }
    ]
}

# 五、上传文件(图片)到阿里云存储OSS--Stream 流模式(完整流程和代码)

# 1. 环境说明

运行环境最低版本为以下的版本。

`node -v`:v18.12.1  `node版本` 
`npm list egg`:
                egg-mock@5.10.9
                │ └── egg@3.17.5 deduped   
                └── egg@3.17.5  `egg版本`           
`npm -v`8.19.2  `npm版本` 

# 2. 安装阿里云OSS SDK命令

npm install ali-oss --save

# 3. 路由

app/router/admin/shop.js

// 图片上传阿里云
router.post('/shop/admin/image/uploadAliyun',controller.admin.image.uploadAliyunOSS);

# 4.配置:config/config.default.js

...
  //配置上传文件相关
  config.multipart = {
    fileSize: '50mb', //最大上传大小50MB
    mode: 'stream', //上传文件模式,stream 流模式 file 模式
    fileExtensions: ['.xls', '.txt', '.jpg', '.JPG', '.png', '.PNG',
      '.gif', '.GIF', '.jpeg', '.JPEG', 
      '.doc', '.docx', '.pdf', '.PDF', '.xlsx', '.XLSX'],//文件格式自定义
    cleanSchedule: {
      cron: '0 30 4 * * *', // 每天凌晨4:30清理临时文件
    },
    
  };

  // 上传文件到阿里云oss的配置
  config.oss = {
    client: {
      accessKeyId: '你自己的阿里云oss的accessKeyId',
      accessKeySecret: '你自己的阿里云oss的accessKeySecret',
      bucket: 'thinkphp-eggjs', //你的bucket名称
      endpoint: 'oss-cn-hangzhou.aliyuncs.com', //你的bucket所在地域
      timeout: '60s', //上传文件超时时间
    }
  };
...

# 5. 扩展方法

app/extend/context.js

'use strict';

// 阿里云OSS SDK
const OSS = require('ali-oss');
// node系统模块
const path = require('path');
const crypto = require('crypto');
const fs = require('fs');
//使用 pump 确保流正确写入,必须写入临时文件后才能上传到 OSS
//内存管理优化,使用 pump 控制流式写入,每个文件单独处理,避免内存峰值,及时清理临时文件
const pump = require('mz-modules/pump');

module.exports = {
    ...
    // 通用文件上传到阿里云OSS方法--Stream 流模式
    /**
     * 通用文件上传到阿里云OSS方法--Stream 流模式
     * @param {string} fieldName - 上传文件的字段名
     * @param {number} imageClassId - 图片分类ID,默认为0
     * @param {string} prefix - 阿里云oss的Bucket中最外层文件夹名称,默认为'images'
     * @returns {Array} - 上传结果数组,每个元素对象包含上传文件的URL、路径、分类ID和创建时间
     */
    async uploadOSS_Stream(fieldName, imageClassId = 0, prefix = 'images') {
        const { app } = this;
        const client = new OSS(app.config.oss.client);
        const results = [];
        const tmpFiles = []; // 记录临时文件路径

        try {
            //通过 ctx.multipart() 创建迭代器,循环处理每个上传的文件流
            const multipart = this.multipart();
            let stream;

            // 流式迭代处理
            while ((stream = await multipart()) != null) {
            // 过滤非目标字段,精确过滤目标上传字段,支持与其他表单字段共存
            if (stream.fieldname !== fieldName) {
                continue;
            }

            // 生成唯一文件名
            const timestamp = Date.now();
            const randomStr = crypto.randomBytes(6).toString('hex');
            const extname = path.extname(stream.filename).toLowerCase();
            const filename = `${timestamp}_${randomStr}${extname}`;

            // 创建日期目录
            const now = new Date();
            const datePath = [
                now.getFullYear(),
                String(now.getMonth() + 1).padStart(2, '0'),
                String(now.getDate()).padStart(2, '0')
            ].join('');

            // 构造完整路径
            const ossPath = `${prefix}/${datePath}/${filename}`;
            
            // 创建临时文件路径
            const tmpFilePath = path.join(
                app.config.multipart.tmpdir, 
                `${timestamp}_${randomStr}${extname}`
            );
            tmpFiles.push(tmpFilePath);

            // 写入临时文件,使用 pump 确保流正确写入,必须写入临时文件后才能上传到 OSS
            const writeStream = fs.createWriteStream(tmpFilePath);
            await pump(stream, writeStream);

            // 上传到OSS,直接从临时文件创建可读流上传,避免内存中缓存大文件
            const ossRes = await client.put(
                ossPath, 
                fs.createReadStream(tmpFilePath)
            );

            results.push({
                url: ossRes.url,
                path: ossPath,
                image_class_id: Number(imageClassId),
                create_time: Math.floor(Date.now() / 1000)
            });
            }

            // 清理临时文件
            await Promise.all(
            tmpFiles.map(file => fs.promises.unlink(file))
            );

            return results;
        } catch (err) {
            // 异常时清理所有临时文件
            await Promise.all(
            tmpFiles.map(file => 
                fs.promises.unlink(file).catch(() => {})
            )
            );
            throw err;
        }
    },
};

# 6. 控制器

app/controller/admin/image.js

...
class ImageController extends Controller {
    // 通用文件上传到阿里云OSS方法
    async uploadAliyunOSS() {
        const { ctx,app } = this;
        try {
            
            // 获取分类ID(通过字段传递)
            const imageClassId = ctx.request.body.image_class_id || 0;

            // 通用文件上传到阿里云OSS方法--File模式
            // const result = await ctx.uploadOSS_File('img', imageClassId, 'images');
            // 通用文件上传到阿里云OSS方法--Stream 流模式
            const result = await ctx.uploadOSS_Stream('img', imageClassId, 'images');

            ctx.body = {
                code: 200,
                msg: '上传成功',
                data: result
            };
            
        } catch (error) {
            ctx.status = 500;
            ctx.body = {
                code: 500,
                msg: error.message,
                data: []
            };
        }
    }
}
...

# 7.postman测试方法

# 1. 单图上传测试:

方法:POST
URL:http://127.0.0.1:7001/shop/admin/image/uploadAliyun
Body选择form-data格式:
添加字段:
Key: image_class_id,Value: 0类型Text
Key: img,Value: 选择文件(类型File

# 2. 多图上传测试:

方法:POST
URL:http://127.0.0.1:7001/shop/admin/image/uploadAliyun
Body选择form-data格式:
添加字段:
Key: image_class_id,Value: 0类型Text
Key: img,Value: 选择多个文件(点击字段右侧下拉箭头选择类型File后,可多选文件)
或者 填写多个img字段,每个字段选择一个文件(类型File

# 8. 返回实例

// 单图
{
    "code": 200,
    "msg": "上传成功",
    "data": [
        {
            "url": "http://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250417/1744866732481_20f90b51544f.png",
            "path": "images/20250417/1744866732481_20f90b51544f.png",
            "image_class_id": 0,
            "create_time": 1744866732
        }
    ]
}
// 多图
{
    "code": 200,
    "msg": "上传成功",
    "data": [
        {
            "url": "http://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250417/1744866760867_f376ca377d65.png",
            "path": "images/20250417/1744866760867_f376ca377d65.png",
            "image_class_id": 0,
            "create_time": 1744866761
        },
        {
            "url": "http://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250417/1744866760867_85681992b9b1.png",
            "path": "images/20250417/1744866760867_85681992b9b1.png",
            "image_class_id": 0,
            "create_time": 1744866761
        },
        {
            "url": "http://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250417/1744866760867_5aab3128bc21.png",
            "path": "images/20250417/1744866760867_5aab3128bc21.png",
            "image_class_id": 0,
            "create_time": 1744866761
        }
    ]
}

# 9.常见问题解决方案

# 1. 上传大文件时内存溢出

// config/config.default.js
config.multipart = {
  // 其他配置...
  fileModeMatch: null // 禁用文件模式匹配
};

# 2. 临时文件清理失败

// 在 app.js 中添加全局清理
app.beforeStart(async () => {
  const tmpdir = app.config.multipart.tmpdir;
  setInterval(() => {
    fs.readdir(tmpdir, (err, files) => {
      files.forEach(file => {
        fs.unlink(path.join(tmpdir, file), () => {});
      });
    });
  }, 3600 * 1000); // 每小时清理一次
});

# 3. 上传速度优化

// 使用分片上传
const ossRes = await client.multipartUpload(ossPath, tmpFilePath, {
  parallel: 4,
  partSize: 1024 * 1024
});
更新时间: 2025年4月17日星期四晚上9点36分