Procházet zdrojové kódy

图片上传优化

wuyi před 1 měsícem
rodič
revize
8ae54a6f3e

+ 11 - 8
src/controllers/file.controller.ts

@@ -14,7 +14,7 @@ export class FileController {
   async uploadZip(request: FastifyRequest, reply: FastifyReply) {
     try {
       const data = await request.file()
-      
+
       if (!data) {
         return reply.code(400).send({ message: '请选择要上传的ZIP文件' })
       }
@@ -52,7 +52,7 @@ export class FileController {
   async uploadFile(request: FastifyRequest, reply: FastifyReply) {
     try {
       const data = await request.file()
-      
+
       if (!data) {
         return reply.code(400).send({ message: '请选择要上传的文件' })
       }
@@ -86,7 +86,7 @@ export class FileController {
   async uploadImage(request: FastifyRequest, reply: FastifyReply) {
     try {
       const data = await request.file()
-      
+
       if (!data) {
         return reply.code(400).send({ message: '请选择要上传的图片' })
       }
@@ -95,7 +95,7 @@ export class FileController {
       const filename = data.filename
 
       const result = await this.fileService.uploadImage(buffer, filename, {
-        maxSize: 5 * 1024 * 1024 // 5MB
+        maxSize: 15 * 1024 * 1024 // 15MB
       })
 
       return reply.send({
@@ -119,7 +119,7 @@ export class FileController {
   async uploadDocument(request: FastifyRequest, reply: FastifyReply) {
     try {
       const data = await request.file()
-      
+
       if (!data) {
         return reply.code(400).send({ message: '请选择要上传的文档' })
       }
@@ -216,13 +216,16 @@ export class FileController {
 
       // 获取文件信息
       const fileInfo = await this.fileService.getFileInfo(key)
-      
+
       // 获取文件内容
       const fileBuffer = await this.fileService.downloadFile(key)
 
       // 设置响应头
       reply.header('Content-Type', fileInfo.res.headers['content-type'] || 'application/octet-stream')
-      reply.header('Content-Disposition', `attachment; filename="${encodeURIComponent(key.split('/').pop() || 'file')}"`)
+      reply.header(
+        'Content-Disposition',
+        `attachment; filename="${encodeURIComponent(key.split('/').pop() || 'file')}"`
+      )
       reply.header('Content-Length', fileInfo.res.headers['content-length'] || fileBuffer.length)
       reply.header('Cache-Control', 'no-cache')
 
@@ -235,4 +238,4 @@ export class FileController {
       })
     }
   }
-} 
+}

+ 6 - 24
src/routes/file.routes.ts

@@ -6,31 +6,16 @@ export default async function fileRoutes(fastify: FastifyInstance) {
   const fileController = new FileController(fastify)
 
   // 上传ZIP文件
-  fastify.post(
-    '/upload/zip',
-    { onRequest: [authenticate] },
-    fileController.uploadZip.bind(fileController)
-  )
+  fastify.post('/upload/zip', { onRequest: [authenticate] }, fileController.uploadZip.bind(fileController))
 
   // 上传文件
-  fastify.post(
-    '/upload',
-    fileController.uploadFile.bind(fileController)
-  )
+  fastify.post('/upload', fileController.uploadFile.bind(fileController))
 
   // 上传图片
-  fastify.post(
-    '/upload/image',
-    { onRequest: [authenticate] },
-    fileController.uploadImage.bind(fileController)
-  )
+  fastify.post('/upload/image', fileController.uploadImage.bind(fileController))
 
   // 上传文档
-  fastify.post(
-    '/upload/document',
-    { onRequest: [authenticate] },
-    fileController.uploadDocument.bind(fileController)
-  )
+  fastify.post('/upload/document', { onRequest: [authenticate] }, fileController.uploadDocument.bind(fileController))
 
   // 删除文件
   fastify.delete<{ Params: { key: string } }>(
@@ -47,8 +32,5 @@ export default async function fileRoutes(fastify: FastifyInstance) {
   )
 
   // 下载文件
-  fastify.post<{ Body: { key: string } }>(
-    '/download',
-    fileController.downloadFile.bind(fileController)
-  )
-} 
+  fastify.post<{ Body: { key: string } }>('/download', fileController.downloadFile.bind(fileController))
+}

+ 10 - 21
src/services/file.service.ts

@@ -25,7 +25,7 @@ export class FileService {
 
   constructor(app: FastifyInstance) {
     const config = app.config
-    
+
     this.bucket = config.OSS_BUCKET
     this.defaultFolder = config.UPLOAD_FOLDER
     this.ossClient = new OSS({
@@ -57,11 +57,7 @@ export class FileService {
    * @param options 上传选项
    * @returns 上传结果
    */
-  async uploadZip(
-    buffer: Buffer,
-    originalName: string,
-    options: FileUploadOptions = {}
-  ): Promise<UploadResult> {
+  async uploadZip(buffer: Buffer, originalName: string, options: FileUploadOptions = {}): Promise<UploadResult> {
     // 验证文件类型
     if (!originalName.toLowerCase().endsWith('.zip')) {
       throw new Error('只支持ZIP格式的压缩包')
@@ -75,12 +71,12 @@ export class FileService {
     // 生成文件名
     const extension = path.extname(originalName)
     const filename = options.filename || `${randomUUID()}${extension}`
-    
+
     // 获取当前日期文件夹
     const dateFolder = this.getDateFolder()
     const baseFolder = options.folder || this.defaultFolder
     const targetFolder = `${baseFolder}/${dateFolder}`
-    
+
     // 构建文件路径
     const key = `${targetFolder}/${filename}`
 
@@ -131,12 +127,12 @@ export class FileService {
     // 生成文件名
     const extension = path.extname(originalName)
     const filename = options.filename || `${randomUUID()}${extension}`
-    
+
     // 获取当前日期文件夹
     const dateFolder = this.getDateFolder()
     const baseFolder = options.folder || this.defaultFolder
     const targetFolder = `${baseFolder}/${dateFolder}`
-    
+
     // 构建文件路径
     const key = `${targetFolder}/${filename}`
 
@@ -172,20 +168,13 @@ export class FileService {
     originalName: string,
     options: Omit<FileUploadOptions, 'allowedTypes'> = {}
   ): Promise<UploadResult> {
-    const imageTypes = [
-      'image/jpeg',
-      'image/jpg',
-      'image/png',
-      'image/gif',
-      'image/webp',
-      'image/svg+xml'
-    ]
+    const imageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
 
     return this.uploadFile(buffer, originalName, 'image/jpeg', {
       ...options,
       folder: options.folder || this.defaultFolder,
       allowedTypes: imageTypes,
-      maxSize: options.maxSize || 10 * 1024 * 1024 // 默认10MB
+      maxSize: options.maxSize || 15 * 1024 * 1024 // 默认15MB
     })
   }
 
@@ -281,7 +270,7 @@ export class FileService {
    * @param keys 文件key数组
    * @returns 删除结果
    */
-  async deleteFiles(keys: string[]): Promise<{ success: string[], failed: string[] }> {
+  async deleteFiles(keys: string[]): Promise<{ success: string[]; failed: string[] }> {
     const result = {
       success: [] as string[],
       failed: [] as string[]
@@ -312,4 +301,4 @@ export class FileService {
       throw new Error(`文件下载失败: ${error instanceof Error ? error.message : '未知错误'}`)
     }
   }
-} 
+}

+ 7 - 1
src/services/pet-info.service.ts

@@ -57,7 +57,13 @@ export class PetInfoService {
     })
 
     if (!petInfo) {
-      throw new Error('宠物/物品信息不存在')
+      const created = this.petInfoRepository.create({
+        ...data,
+        qrCodeId: qrCodeEntity.id
+      })
+      const result = await this.petInfoRepository.save(created)
+      await this.qrCodeService.activateQrCode(qrCodeEntity.id)
+      return result
     }
 
     await this.petInfoRepository.update(petInfo.id, data)

+ 23 - 4
src/services/qr-code.service.ts

@@ -4,18 +4,22 @@ import { QrCode, QrType } from '../entities/qr-code.entity'
 import { PersonInfo } from '../entities/person-info.entity'
 import { PetInfo } from '../entities/pet-info.entity'
 import { PaginationResponse } from '../dto/common.dto'
-import bcrypt from 'bcryptjs'
 import { randomBytes } from 'crypto'
+import { FileService } from './file.service'
 
 export class QrCodeService {
   private qrCodeRepository: Repository<QrCode>
   private personInfoRepository: Repository<PersonInfo>
   private petInfoRepository: Repository<PetInfo>
+  private fileService: FileService
+  private app: FastifyInstance
 
   constructor(app: FastifyInstance) {
     this.qrCodeRepository = app.dataSource.getRepository(QrCode)
     this.personInfoRepository = app.dataSource.getRepository(PersonInfo)
     this.petInfoRepository = app.dataSource.getRepository(PetInfo)
+    this.fileService = new FileService(app)
+    this.app = app
   }
 
   /**
@@ -51,11 +55,10 @@ export class QrCodeService {
     for (let i = 0; i < quantity; i++) {
       const qrCode = this.generateQrCode()
       const maintenanceCode = this.generateMaintenanceCode()
-      const hashedMaintenanceCode = await bcrypt.hash(maintenanceCode, 10)
 
       const entity = this.qrCodeRepository.create({
         qrCode,
-        maintenanceCode: hashedMaintenanceCode,
+        maintenanceCode,
         qrType,
         isActivated: false,
         scanCount: 0
@@ -80,7 +83,7 @@ export class QrCodeService {
     if (!entity) {
       return false
     }
-    return bcrypt.compare(maintenanceCode, entity.maintenanceCode)
+    return maintenanceCode === entity.maintenanceCode
   }
 
   /**
@@ -99,6 +102,22 @@ export class QrCodeService {
       info = await this.petInfoRepository.findOne({ where: { qrCodeId: entity.id } })
     }
 
+    // 处理图片签名URL
+    if (info && info.photoUrl) {
+      try {
+        if (!info.photoUrl.startsWith('http')) {
+          info.photoUrl = await this.fileService.getSignedUrl(info.photoUrl, 3600)
+        } else {
+          const urlObj = new URL(info.photoUrl)
+          const key = urlObj.pathname.substring(1)
+          info.photoUrl = await this.fileService.getSignedUrl(key, 3600)
+        }
+      } catch (error) {
+        this.app.log.error('Failed to generate signed URL:', error)
+        info.photoUrl = ''
+      }
+    }
+
     return {
       id: entity.id,
       qrCode: entity.qrCode,