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