Skip to content

Commit b0f9319

Browse files
aixierclaude
andcommitted
feat: 添加Pod2Post文件上传并返回OSS URL功能
- 新增 /api/generate/pod2post/upload-and-return-url 接口 - 支持单文件和批量文件上传 - 上传后返回带签名的长期有效OSS URL - 支持用户隔离和自定义文件夹 - 修改 resourceUploader.js - 修正OSS URL生成格式 - 使用正确的bucket和region构建完整URL - 特性: - 支持多种文件类型(图片、文档、文本等) - 生成10年有效期的签名URL - 文件名自动添加时间戳避免冲突 - 本地记录上传历史 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2f0c4ec commit b0f9319

3 files changed

Lines changed: 393 additions & 7 deletions

File tree

terminal-backend/src/routes/generate/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import pod2postCdnRoutes from './pod2postCdn.js'
1515
import pod2postPicRoutes from './pod2postPic.js'
1616
import pod2postResourcesRoutes from './pod2postResources.js'
1717
import pod2postWriteTextRoutes from './pod2postWriteText.js'
18+
import pod2postUploadAndReturnUrlRoutes from './pod2postUploadAndReturnUrl.js'
1819
import templateRoutes from './templates.js'
1920
import statusRoutes from './status.js'
2021
import claudeRoutes from './claude.js'
@@ -49,6 +50,7 @@ router.use('/pod2post/cdn', pod2postCdnRoutes) // POST /api/generate/pod2post/cd
4950
router.use('/pod2post/pic', pod2postPicRoutes) // POST /api/generate/pod2post/pic (照片上传)
5051
router.use('/pod2post/resources', pod2postResourcesRoutes) // POST /api/generate/pod2post/resources (参考文档上传)
5152
router.use('/pod2post/write-text', pod2postWriteTextRoutes) // POST /api/generate/pod2post/write-text (文本文件写入)
53+
router.use('/pod2post/upload-and-return-url', pod2postUploadAndReturnUrlRoutes) // POST /api/generate/pod2post/upload-and-return-url (上传并返回OSS URL)
5254

5355
// 模板和状态路由
5456
router.use('/templates', templateRoutes) // GET /api/generate/templates
@@ -78,6 +80,7 @@ router.get('/health', (req, res) => {
7880
pod2postPic: 'active',
7981
pod2postResources: 'active',
8082
pod2postWriteText: 'active',
83+
pod2postUploadAndReturnUrl: 'active',
8184
templates: 'active',
8285
status: 'active',
8386
claude: 'active'
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
/**
2+
* Pod2Post 文件上传并返回OSS URL接口
3+
*
4+
* 功能:
5+
* 1. 接收文件上传
6+
* 2. 上传到OSS
7+
* 3. 返回可访问的OSS URL
8+
*
9+
* @author AI Terminal Team
10+
* @version 1.0.0
11+
* @created 2025-01-13
12+
*/
13+
14+
import express from 'express'
15+
import path from 'path'
16+
import fs from 'fs/promises'
17+
import multer from 'multer'
18+
import { authenticateUserOrDefault } from '../../middleware/userAuth.js'
19+
import userService from '../../services/userService.js'
20+
import { OSSService } from '../../services/oss/index.cjs'
21+
22+
const router = express.Router()
23+
24+
// 配置multer用于文件上传
25+
const storage = multer.memoryStorage()
26+
const upload = multer({
27+
storage,
28+
limits: {
29+
fileSize: 50 * 1024 * 1024 // 50MB
30+
},
31+
fileFilter: (req, file, cb) => {
32+
// 允许的文件类型
33+
const allowedTypes = [
34+
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
35+
'text/plain', 'text/markdown', 'application/json',
36+
'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
37+
]
38+
39+
if (allowedTypes.includes(file.mimetype)) {
40+
cb(null, true)
41+
} else {
42+
cb(new Error(`不支持的文件类型: ${file.mimetype}`), false)
43+
}
44+
}
45+
})
46+
47+
/**
48+
* 上传单个文件并返回OSS URL
49+
* POST /api/generate/pod2post/upload-and-return-url
50+
*
51+
* Body参数 (multipart/form-data):
52+
* - file: 文件 (必填)
53+
* - task_id: 任务ID (必填)
54+
* - folder: 文件夹路径 (可选,默认: '')
55+
* - token: 用户token (可选)
56+
*/
57+
router.post('/',
58+
upload.single('file'),
59+
authenticateUserOrDefault,
60+
async (req, res) => {
61+
const { task_id, folder = '', token } = req.body
62+
const uploadedFile = req.file
63+
64+
console.log('[Pod2PostUploadAndReturnUrl] ==================== UPLOAD REQUEST ====================')
65+
console.log('[Pod2PostUploadAndReturnUrl] Task ID:', task_id)
66+
console.log('[Pod2PostUploadAndReturnUrl] Folder:', folder)
67+
console.log('[Pod2PostUploadAndReturnUrl] File:', uploadedFile?.originalname)
68+
console.log('[Pod2PostUploadAndReturnUrl] File size:', uploadedFile?.size)
69+
console.log('[Pod2PostUploadAndReturnUrl] Token:', token ? `${token.substring(0, 15)}...` : 'none')
70+
71+
try {
72+
// 1. 参数验证
73+
if (!task_id || !task_id.startsWith('pod2post_')) {
74+
return res.status(400).json({
75+
code: 400,
76+
success: false,
77+
message: '参数错误: task_id 格式不正确,应为 pod2post_{timestamp}_{random}'
78+
})
79+
}
80+
81+
if (!uploadedFile) {
82+
return res.status(400).json({
83+
code: 400,
84+
success: false,
85+
message: '参数错误: 未找到上传的文件'
86+
})
87+
}
88+
89+
// 2. 处理用户认证
90+
let targetUser = req.user
91+
92+
if (token) {
93+
const tokenUser = await userService.findUserByToken(token)
94+
if (tokenUser) {
95+
targetUser = tokenUser
96+
console.log(`[Pod2PostUploadAndReturnUrl] Using token-specified user: ${tokenUser.username}`)
97+
}
98+
}
99+
100+
// 3. 初始化OSS服务
101+
let ossService
102+
try {
103+
ossService = new OSSService('default')
104+
console.log('[Pod2PostUploadAndReturnUrl] OSS service initialized')
105+
} catch (error) {
106+
console.error('[Pod2PostUploadAndReturnUrl] OSS initialization failed:', error)
107+
return res.status(500).json({
108+
code: 500,
109+
success: false,
110+
message: 'OSS服务初始化失败',
111+
error: error.message
112+
})
113+
}
114+
115+
// 4. 构建OSS路径
116+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
117+
const fileName = `${timestamp}_${uploadedFile.originalname}`
118+
const ossPathParts = ['pod2post', targetUser.username, task_id]
119+
120+
if (folder) {
121+
// 清理并添加文件夹路径
122+
const cleanFolder = folder.replace(/^\/+|\/+$/g, '').replace(/\\/g, '/')
123+
ossPathParts.push(...cleanFolder.split('/'))
124+
}
125+
126+
ossPathParts.push(fileName)
127+
const ossPath = ossPathParts.join('/')
128+
129+
console.log('[Pod2PostUploadAndReturnUrl] OSS path:', ossPath)
130+
131+
// 5. 上传到OSS
132+
let uploadResult
133+
try {
134+
// 使用Buffer上传
135+
uploadResult = await ossService.uploadBuffer(uploadedFile.buffer, ossPath, {
136+
headers: {
137+
'Content-Type': uploadedFile.mimetype,
138+
'Cache-Control': 'public, max-age=31536000',
139+
'Content-Disposition': `inline; filename="${encodeURIComponent(uploadedFile.originalname)}"`
140+
}
141+
})
142+
143+
console.log('[Pod2PostUploadAndReturnUrl] OSS upload success:', uploadResult.url)
144+
} catch (error) {
145+
console.error('[Pod2PostUploadAndReturnUrl] OSS upload failed:', error)
146+
return res.status(500).json({
147+
code: 500,
148+
success: false,
149+
message: '文件上传到OSS失败',
150+
error: error.message
151+
})
152+
}
153+
154+
// 6. 生成带签名的访问URL(长期有效)
155+
let publicUrl
156+
try {
157+
// 生成一个长期有效的签名URL(例如:10年)
158+
const signedResult = await ossService.generateSignedUrl(ossPath, 10 * 365 * 24 * 3600) // 10年
159+
publicUrl = signedResult.url || signedResult
160+
console.log('[Pod2PostUploadAndReturnUrl] Signed URL generated:', publicUrl.substring(0, 100) + '...')
161+
} catch (urlError) {
162+
console.warn('[Pod2PostUploadAndReturnUrl] Failed to generate signed URL:', urlError.message)
163+
// 如果生成签名URL失败,使用OSS返回的基础URL
164+
publicUrl = uploadResult.url
165+
}
166+
167+
// 7. 保存文件记录到本地(可选)
168+
try {
169+
const taskPath = userService.getUserCardPath(targetUser.username, task_id)
170+
const recordPath = path.join(taskPath, 'upload_records.json')
171+
172+
let records = []
173+
try {
174+
const existingData = await fs.readFile(recordPath, 'utf-8')
175+
records = JSON.parse(existingData)
176+
} catch {
177+
// 文件不存在,使用空数组
178+
}
179+
180+
// 添加新记录
181+
records.push({
182+
originalName: uploadedFile.originalname,
183+
ossPath: ossPath,
184+
ossUrl: uploadResult.url,
185+
size: uploadedFile.size,
186+
mimetype: uploadedFile.mimetype,
187+
uploadedAt: new Date().toISOString(),
188+
folder: folder
189+
})
190+
191+
// 保存记录
192+
await fs.writeFile(recordPath, JSON.stringify(records, null, 2))
193+
console.log('[Pod2PostUploadAndReturnUrl] Upload record saved')
194+
} catch (recordError) {
195+
console.warn('[Pod2PostUploadAndReturnUrl] Failed to save upload record:', recordError.message)
196+
}
197+
198+
// 8. 返回成功响应
199+
res.json({
200+
code: 200,
201+
success: true,
202+
message: '文件上传成功',
203+
data: {
204+
originalName: uploadedFile.originalname,
205+
ossPath: ossPath,
206+
ossUrl: uploadResult.url, // OSS返回的基础URL
207+
publicUrl: publicUrl, // 带签名的可访问URL(长期有效)
208+
size: uploadedFile.size,
209+
mimetype: uploadedFile.mimetype,
210+
uploadedAt: new Date().toISOString(),
211+
taskId: task_id,
212+
folder: folder,
213+
username: targetUser.username,
214+
etag: uploadResult.etag
215+
}
216+
})
217+
218+
} catch (error) {
219+
console.error('[Pod2PostUploadAndReturnUrl] Upload failed:', error)
220+
return res.status(500).json({
221+
code: 500,
222+
success: false,
223+
message: '文件上传失败',
224+
error: process.env.NODE_ENV === 'development' ? error.message : '服务器内部错误'
225+
})
226+
}
227+
}
228+
)
229+
230+
/**
231+
* 批量上传文件并返回OSS URL列表
232+
* POST /api/generate/pod2post/batch-upload-and-return-url
233+
*
234+
* Body参数 (multipart/form-data):
235+
* - files: 多个文件 (必填)
236+
* - task_id: 任务ID (必填)
237+
* - folder: 文件夹路径 (可选)
238+
* - token: 用户token (可选)
239+
*/
240+
router.post('/batch-upload-and-return-url',
241+
upload.array('files', 20), // 最多20个文件
242+
authenticateUserOrDefault,
243+
async (req, res) => {
244+
const { task_id, folder = '', token } = req.body
245+
const uploadedFiles = req.files || []
246+
247+
console.log('[Pod2PostBatchUpload] ==================== BATCH UPLOAD REQUEST ====================')
248+
console.log('[Pod2PostBatchUpload] Task ID:', task_id)
249+
console.log('[Pod2PostBatchUpload] Folder:', folder)
250+
console.log('[Pod2PostBatchUpload] Files count:', uploadedFiles.length)
251+
console.log('[Pod2PostBatchUpload] Token:', token ? `${token.substring(0, 15)}...` : 'none')
252+
253+
try {
254+
// 参数验证
255+
if (!task_id || !task_id.startsWith('pod2post_')) {
256+
return res.status(400).json({
257+
code: 400,
258+
success: false,
259+
message: '参数错误: task_id 格式不正确'
260+
})
261+
}
262+
263+
if (uploadedFiles.length === 0) {
264+
return res.status(400).json({
265+
code: 400,
266+
success: false,
267+
message: '参数错误: 未找到上传的文件'
268+
})
269+
}
270+
271+
// 处理用户认证
272+
let targetUser = req.user
273+
if (token) {
274+
const tokenUser = await userService.findUserByToken(token)
275+
if (tokenUser) {
276+
targetUser = tokenUser
277+
}
278+
}
279+
280+
// 初始化OSS服务
281+
let ossService
282+
try {
283+
ossService = new OSSService('default')
284+
} catch (error) {
285+
return res.status(500).json({
286+
code: 500,
287+
success: false,
288+
message: 'OSS服务初始化失败'
289+
})
290+
}
291+
292+
// 批量上传
293+
const results = []
294+
const errors = []
295+
296+
for (const [index, file] of uploadedFiles.entries()) {
297+
try {
298+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
299+
const fileName = `${timestamp}_${file.originalname}`
300+
const ossPathParts = ['pod2post', targetUser.username, task_id]
301+
302+
if (folder) {
303+
const cleanFolder = folder.replace(/^\/+|\/+$/g, '').replace(/\\/g, '/')
304+
ossPathParts.push(...cleanFolder.split('/'))
305+
}
306+
307+
ossPathParts.push(fileName)
308+
const ossPath = ossPathParts.join('/')
309+
310+
// 上传到OSS
311+
const uploadResult = await ossService.uploadBuffer(file.buffer, ossPath, {
312+
headers: {
313+
'Content-Type': file.mimetype,
314+
'Cache-Control': 'public, max-age=31536000'
315+
}
316+
})
317+
318+
// 生成带签名的访问URL(长期有效)
319+
let publicUrl
320+
try {
321+
const signedResult = await ossService.generateSignedUrl(ossPath, 10 * 365 * 24 * 3600) // 10年
322+
publicUrl = signedResult.url || signedResult
323+
} catch (urlError) {
324+
publicUrl = uploadResult.url
325+
}
326+
327+
results.push({
328+
originalName: file.originalname,
329+
ossPath: ossPath,
330+
ossUrl: uploadResult.url, // OSS返回的基础URL
331+
publicUrl: publicUrl, // 带签名的可访问URL
332+
size: file.size,
333+
mimetype: file.mimetype,
334+
uploadedAt: new Date().toISOString()
335+
})
336+
337+
console.log(`[Pod2PostBatchUpload] Uploaded ${index + 1}/${uploadedFiles.length}: ${file.originalname}`)
338+
} catch (error) {
339+
console.error(`[Pod2PostBatchUpload] Failed to upload ${file.originalname}:`, error.message)
340+
errors.push({
341+
originalName: file.originalname,
342+
error: error.message
343+
})
344+
}
345+
}
346+
347+
// 返回结果
348+
res.json({
349+
code: 200,
350+
success: true,
351+
message: `批量上传完成: ${results.length} 成功, ${errors.length} 失败`,
352+
data: {
353+
taskId: task_id,
354+
folder: folder,
355+
username: targetUser.username,
356+
uploaded: results,
357+
failed: errors
358+
}
359+
})
360+
361+
} catch (error) {
362+
console.error('[Pod2PostBatchUpload] Batch upload failed:', error)
363+
return res.status(500).json({
364+
code: 500,
365+
success: false,
366+
message: '批量上传失败',
367+
error: error.message
368+
})
369+
}
370+
}
371+
)
372+
373+
export default router

0 commit comments

Comments
 (0)