This commit is contained in:
张成
2025-11-24 13:23:42 +08:00
commit 5d7444cd65
156 changed files with 50653 additions and 0 deletions

View File

@@ -0,0 +1,332 @@
const OSS = require('ali-oss')
const fs = require('fs')
const config = require('../../config/config.js');
const uuid = require('node-uuid')
const logs = require('../middleware/logProxy')
/**
* OSS 文件上传服务
* 统一管理文件上传、存储路径、文件类型等
*/
class OSSToolService {
constructor() {
const { accessKeyId, accessKeySecret, bucket, region } = config.oos;
this.client = new OSS({
region,
accessKeyId,
accessKeySecret,
bucket,
})
// 基础存储路径前缀
this.basePrefix = 'front/work'
// 文件类型映射
this.fileTypeMap = {
// 图片类型
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/webp': 'webp',
'image/svg+xml': 'svg',
// 视频类型
'video/mp4': 'mp4',
'video/avi': 'avi',
'video/mov': 'mov',
'video/wmv': 'wmv',
'video/flv': 'flv',
'video/webm': 'webm',
'video/mkv': 'mkv',
// 音频类型
'audio/mp3': 'mp3',
'audio/wav': 'wav',
'audio/aac': 'aac',
'audio/ogg': 'ogg',
'audio/flac': 'flac',
// 文档类型
'application/pdf': 'pdf',
'application/msword': 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
'application/vnd.ms-excel': 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
'application/vnd.ms-powerpoint': 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
'text/plain': 'txt',
'text/html': 'html',
'text/css': 'css',
'application/javascript': 'js',
'application/json': 'json'
}
}
/**
* 获取文件后缀名
* @param {Object} file - 文件对象(兼容 formidable 格式)
* @returns {string} 文件后缀名
*/
getFileSuffix(file) {
// 优先使用 MIME 类型判断(兼容 type 和 mimetype
const mimeType = file.mimetype || file.type
if (mimeType && this.fileTypeMap[mimeType]) {
return this.fileTypeMap[mimeType]
}
// 备用方案:从文件名获取(兼容 originalFilename 和 name
const fileName = file.originalFilename || file.name
if (fileName) {
const lastIndex = fileName.lastIndexOf('.')
if (lastIndex > -1) {
return fileName.substring(lastIndex + 1).toLowerCase()
}
}
return 'bin'
}
/**
* 获取文件存储路径
* @param {Object} file - 文件对象(兼容 formidable 格式)
* @param {string} category - 存储分类
* @returns {string} 完整的存储路径
*/
getStoragePath(file, category = 'files') {
const suffix = this.getFileSuffix(file)
const uid = uuid.v4()
// 根据文件类型确定子路径(兼容 mimetype 和 type
let subPath = category
const mimeType = file.mimetype || file.type
if (mimeType) {
if (mimeType.startsWith('image/')) {
subPath = 'images'
} else if (mimeType.startsWith('video/')) {
subPath = 'videos'
} else if (mimeType.startsWith('audio/')) {
subPath = 'audios'
} else if (mimeType.startsWith('application/') || mimeType.startsWith('text/')) {
subPath = 'documents'
}
}
// 完整路径front/ball/{subPath}/{uid}.{suffix}
return `${this.basePrefix}/${subPath}/${uid}.${suffix}`
}
/**
* 核心文件上传方法
* @param {Object} file - 文件对象(兼容 formidable 格式)
* @param {string} category - 存储分类
* @returns {Object} 上传结果
*/
async uploadFile(file, category = 'files') {
try {
// 兼容不同的文件对象格式filepath 或 path
const filePath = file.filepath || file.path
// 验证文件
if (!file || !filePath) {
return { success: false, error: '无效的文件对象' }
}
const stream = fs.createReadStream(filePath)
const storagePath = this.getStoragePath(file, category)
const suffix = this.getFileSuffix(file)
// 设置 content-type兼容 mimetype 和 type
const contentType = file.mimetype || file.type || 'application/octet-stream'
// 上传到 OSS
const result = await this.client.put(storagePath, stream, {
headers: {
'content-disposition': 'inline',
"content-type": contentType
}
})
if (result.res.status === 200) {
const ossPath = config.ossUrl + '/' + result.name
// 上传成功后删除临时文件
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
} catch (unlinkError) {
logs.error('删除临时文件失败:', unlinkError)
}
return {
success: true,
name: result.name,
path: result.url,
ossPath,
fileType: file.mimetype || file.type,
fileSize: file.size,
originalName: file.originalFilename || file.name,
suffix: suffix,
storagePath: storagePath
}
} else {
return { success: false, error: 'OSS 上传失败' }
}
} catch (error) {
logs.error('文件上传错误:', error)
// 上传失败也要清理临时文件
try {
const filePath = file.filepath || file.path
if (filePath && fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
} catch (unlinkError) {
logs.error('删除临时文件失败:', unlinkError)
}
return { success: false, error: error.message }
}
}
/**
* 上传流数据
* @param {Stream} stream - 文件流
* @param {string} contentType - 内容类型
* @param {string} suffix - 文件后缀
* @returns {Object} 上传结果
*/
async uploadStream(stream, contentType, suffix) {
try {
const uid = uuid.v4()
const storagePath = `${this.basePrefix}/files/${uid}.${suffix}`
const result = await this.client.put(storagePath, stream, {
headers: {
'content-disposition': 'inline',
"content-type": contentType
}
})
if (result.res.status === 200) {
const ossPath = config.ossUrl + result.name
return {
success: true,
name: result.name,
path: result.url,
ossPath,
storagePath: storagePath
}
} else {
return { success: false, error: 'OSS 上传失败' }
}
} catch (error) {
logs.error('流上传错误:', error)
return { success: false, error: error.message }
}
}
/**
* 删除文件
* @param {string} filePath - 文件路径
* @returns {Object} 删除结果
*/
async deleteFile(filePath) {
try {
if (!filePath) {
return { success: false, error: '文件路径不能为空' }
}
// 从完整 URL 中提取相对路径
const relativePath = filePath.replace(config.ossUrl + '/', '')
const result = await this.client.delete(relativePath)
if (result.res.status === 204) {
return { success: true, message: '文件删除成功' }
} else {
return { success: false, error: '文件删除失败' }
}
} catch (error) {
logs.error('文件删除错误:', error)
return { success: false, error: error.message }
}
}
/**
* 获取文件信息
* @param {string} filePath - 文件路径
* @returns {Object} 文件信息
*/
async getFileInfo(filePath) {
try {
if (!filePath) {
return { success: false, error: '文件路径不能为空' }
}
const relativePath = filePath.replace(config.ossUrl + '/', '')
const result = await this.client.head(relativePath)
return {
success: true,
size: result.res.headers['content-length'],
type: result.res.headers['content-type'],
lastModified: result.res.headers['last-modified'],
etag: result.res.headers['etag']
}
} catch (error) {
logs.error('获取文件信息错误:', error)
return { success: false, error: error.message }
}
}
// ==================== 便捷方法 ====================
/**
* 上传图片文件(保持向后兼容)
* @param {Object} file - 图片文件
* @returns {Object} 上传结果
*/
async putImg(file) {
return await this.uploadFile(file, 'images')
}
/**
* 上传视频文件
* @param {Object} file - 视频文件
* @returns {Object} 上传结果
*/
async uploadVideo(file) {
return await this.uploadFile(file, 'videos')
}
/**
* 上传音频文件
* @param {Object} file - 音频文件
* @returns {Object} 上传结果
*/
async uploadAudio(file) {
return await this.uploadFile(file, 'audios')
}
/**
* 上传文档文件
* @param {Object} file - 文档文件
* @returns {Object} 上传结果
*/
async uploadDocument(file) {
return await this.uploadFile(file, 'documents')
}
}
// 创建单例实例
const ossToolService = new OSSToolService()
// 导出实例(保持向后兼容)
module.exports = ossToolService