Kaynağa Gözat

添加二维码更新功能,扩展QueryQrCodeDto以支持所有者信息,更新相关服务和路由配置,优化二维码信息更新逻辑以处理不同类型的关联信息。

wuyi 1 gün önce
ebeveyn
işleme
9a03162161

+ 31 - 32
src/controllers/qr-code.controller.ts

@@ -46,7 +46,7 @@ export class QrCodeController {
    */
   async list(request: FastifyRequest<{ Querystring: QueryQrCodeDto }>, reply: FastifyReply) {
     try {
-      const { qrCode, qrType, isActivated, startDate, endDate, page, pageSize } = request.query
+      const { qrCode, qrType, isActivated, startDate, endDate, ownerId, ownerName, page, pageSize } = request.query
 
       const result = await this.qrCodeService.queryQrCodes(
         qrCode,
@@ -54,6 +54,8 @@ export class QrCodeController {
         isActivated,
         startDate,
         endDate,
+        ownerId,
+        ownerName,
         page,
         pageSize
       )
@@ -65,6 +67,25 @@ export class QrCodeController {
     }
   }
 
+  /**
+   * 更新二维码信息及对应 info
+   */
+  async update(request: FastifyRequest<{ Body: UpdateQrCodeDto }>, reply: FastifyReply) {
+    try {
+      const { id, maintenanceCode, remark, info } = request.body
+
+      await this.qrCodeService.updateQrCode(id, maintenanceCode, remark, info)
+
+      return reply.send({ message: '更新成功' })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '更新失败'
+      const clientErrorKeywords = ['维护码错误', '二维码不存在', '维护码最少', '维护码最多', '维护码只能']
+      const isClientError = clientErrorKeywords.some(keyword => message.includes(keyword))
+      const statusCode = isClientError ? 400 : 500
+      return reply.code(statusCode).send({ message })
+    }
+  }
+
   /**
    * 按日期下载
    */
@@ -153,11 +174,12 @@ export class QrCodeController {
         ipAddress = ipAddress.split(',')[0].trim()
       } else {
         // 如果没有 x-forwarded-for,尝试其他常见的代理头
-        ipAddress = (request.headers['x-real-ip'] as string) || 
-                   (request.headers['cf-connecting-ip'] as string) ||
-                   request.ip || 
-                   request.socket.remoteAddress || 
-                   'unknown'
+        ipAddress =
+          (request.headers['x-real-ip'] as string) ||
+          (request.headers['cf-connecting-ip'] as string) ||
+          request.ip ||
+          request.socket.remoteAddress ||
+          'unknown'
       }
       const userAgent = request.headers['user-agent']
 
@@ -290,7 +312,9 @@ export class QrCodeController {
     } catch (error) {
       const message = error instanceof Error ? error.message : 'Unbinding failed'
       const statusCode =
-        message.includes('not found') || message.includes('already deleted') || message.includes('Please provide') ? 400 : 500
+        message.includes('not found') || message.includes('already deleted') || message.includes('Please provide')
+          ? 400
+          : 500
       return reply.code(statusCode).send({ message })
     }
   }
@@ -311,29 +335,4 @@ export class QrCodeController {
       return reply.code(500).send({ message })
     }
   }
-
-  /**
-   * 更新二维码信息
-   */
-  async update(request: FastifyRequest<{ Body: UpdateQrCodeDto }>, reply: FastifyReply) {
-    try {
-      const { qrCode, maintenanceCode, remark } = request.body
-
-      const updated = await this.qrCodeService.updateQrCode(qrCode, maintenanceCode, { remark })
-
-      return reply.send({
-        message: '更新成功',
-        data: {
-          qrCode: updated.qrCode,
-          remark: updated.remark
-        }
-      })
-    } catch (error) {
-      const message = error instanceof Error ? error.message : '更新失败'
-      const clientErrorKeywords = ['维护码错误', '二维码不存在']
-      const isClientError = clientErrorKeywords.some(keyword => message.includes(keyword))
-      const statusCode = isClientError ? 400 : 500
-      return reply.code(statusCode).send({ message })
-    }
-  }
 }

+ 176 - 4
src/dto/qr-code.dto.ts

@@ -8,9 +8,15 @@ import {
   Min,
   Matches,
   MinLength,
-  MaxLength
+  MaxLength,
+  Length,
+  IsEmail,
+  IsUrl
 } from 'class-validator'
 import { QrType } from '../entities/qr-code.entity'
+import { QueryPetInfoDto } from './pet-info.dto'
+import { calculateObjectSize } from 'typeorm/driver/mongodb/bson.typings'
+import { Gender } from '../entities/person-info.entity'
 
 export class GenerateQrCodeDto {
   @IsEnum(QrType)
@@ -46,7 +52,16 @@ export class QueryQrCodeDto {
   @IsOptional()
   @IsNumber()
   @Min(1)
-  page?: number = 1
+  ownerId?: number
+
+  @IsOptional()
+  @IsString()
+  ownerName?: string
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  page?: number = 0
 
   @IsOptional()
   @IsNumber()
@@ -119,11 +134,168 @@ export class QueryUserQrCodeDto {
 }
 
 export class UpdateQrCodeDto {
+  @IsNumber()
+  id: number
+
+  @IsOptional()
   @IsString()
-  qrCode: string
+  @MinLength(8, { message: '维护码最少8位字符' })
+  @MaxLength(20, { message: '维护码最多20位字符' })
+  @Matches(/^[a-zA-Z0-9]+$/, { message: '维护码只能包含字母和数字' })
+  maintenanceCode?: string
 
+  @IsOptional()
   @IsString()
-  maintenanceCode: string
+  @MaxLength(500)
+  remark?: string
+
+  @IsOptional()
+  info?: PersonInfoDto | PetInfoDto | GoodsInfoDto | LinkInfoDto
+}
+
+export class PetInfoDto {
+  @IsString()
+  maintenanceCode?: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  photoUrl?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 100)
+  name?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 100)
+  contactName?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 20)
+  contactPhone?: string
+
+  @IsOptional()
+  @IsEmail()
+  @MaxLength(100)
+  contactEmail?: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  location?: string
+
+  @IsOptional()
+  @IsBoolean()
+  isVisible?: boolean
+}
+
+export class PersonInfoDto {
+  @IsString()
+  maintenanceCode?: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  photoUrl?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 100)
+  name?: string
+
+  @IsOptional()
+  @IsEnum(Gender)
+  gender?: Gender
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 20)
+  phone?: string
+
+  @IsOptional()
+  @IsString()
+  specialNote?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 100)
+  emergencyContactName?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 20)
+  emergencyContactPhone?: string
+
+  @IsOptional()
+  @IsEmail()
+  @MaxLength(100)
+  emergencyContactEmail?: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  location?: string
+
+  @IsOptional()
+  @IsBoolean()
+  isVisible?: boolean
+}
+
+export class GoodsInfoDto {
+  @IsString()
+  maintenanceCode?: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  photoUrl?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 100)
+  name?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 100)
+  contactName?: string
+
+  @IsOptional()
+  @IsString()
+  @Length(1, 20)
+  contactPhone?: string
+
+  @IsOptional()
+  @IsEmail()
+  @MaxLength(100)
+  contactEmail?: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  location?: string
+
+  @IsOptional()
+  @IsBoolean()
+  isVisible?: boolean
+}
+
+export class LinkInfoDto {
+  @IsString()
+  maintenanceCode?: string
+
+  @IsOptional()
+  @IsString()
+  @IsUrl()
+  @MaxLength(2000)
+  jumpUrl?: string
+
+  @IsOptional()
+  @IsBoolean()
+  isVisible?: boolean
 
   @IsOptional()
   @IsString()

+ 6 - 6
src/routes/qr-code.routes.ts

@@ -47,6 +47,12 @@ export default async function qrCodeRoutes(fastify: FastifyInstance) {
     qrCodeController.list.bind(qrCodeController)
   )
 
+  fastify.post<{ Body: UpdateQrCodeDto }>(
+    '/update',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    qrCodeController.update.bind(qrCodeController)
+  )
+
   fastify.get<{ Querystring: { date: string } }>(
     '/download',
     { onRequest: [hasRole(UserRole.ADMIN)] },
@@ -83,10 +89,4 @@ export default async function qrCodeRoutes(fastify: FastifyInstance) {
     { onRequest: [authenticate] },
     qrCodeController.getUserQrCodes.bind(qrCodeController)
   )
-
-  fastify.put<{ Body: UpdateQrCodeDto }>(
-    '/update',
-    { onRequest: [hasRole(UserRole.ADMIN)] },
-    qrCodeController.update.bind(qrCodeController)
-  )
 }

+ 101 - 24
src/services/qr-code.service.ts

@@ -9,6 +9,10 @@ import { UserQrCode } from '../entities/user-qr-code.entity'
 import { User } from '../entities/user.entity'
 import { PaginationResponse } from '../dto/common.dto'
 import { FileService } from './file.service'
+import { PersonInfoDto } from '../dto/qr-code.dto'
+import { PetInfoDto } from '../dto/qr-code.dto'
+import { GoodsInfoDto } from '../dto/qr-code.dto'
+import { LinkInfoDto } from '../dto/qr-code.dto'
 
 export class QrCodeService {
   private qrCodeRepository: Repository<QrCode>
@@ -162,12 +166,10 @@ export class QrCodeService {
       info = await this.linkInfoRepository.findOne({ where: { qrCodeId: entity.id } })
     }
 
-    // 先设置 isVisible 状态
     if (info) {
       isVisible = info.isVisible ?? true
     }
 
-    // 处理图片签名URL
     if (info && 'photoUrl' in info && info.photoUrl) {
       try {
         if (!info.photoUrl.startsWith('http')) {
@@ -183,7 +185,6 @@ export class QrCodeService {
       }
     }
 
-    // 如果不是管理员且 isVisible 为 false,只返回图片 URL
     if (!isAdmin && info && info.isVisible === false) {
       const photoUrl = 'photoUrl' in info ? info.photoUrl : ''
       info = { photoUrl }
@@ -247,6 +248,8 @@ export class QrCodeService {
     isActivated?: boolean | string,
     startDate?: string,
     endDate?: string,
+    ownerId?: number,
+    ownerName?: string,
     page: number = 0,
     pageSize: number = 20
   ): Promise<PaginationResponse<any>> {
@@ -278,6 +281,20 @@ export class QrCodeService {
       baseQueryBuilder.andWhere('qrCode.createdAt <= :end', { end })
     }
 
+    if (ownerId || ownerName) {
+      baseQueryBuilder
+        .leftJoin(UserQrCode, 'uq', 'uq.qrCodeId = qrCode.id AND uq.isDeleted = :isDeleted', { isDeleted: false })
+        .leftJoin(User, 'owner', 'owner.id = uq.userId')
+
+      if (ownerId) {
+        baseQueryBuilder.andWhere('owner.id = :ownerId', { ownerId })
+      }
+
+      if (ownerName) {
+        baseQueryBuilder.andWhere('owner.name = :ownerName', { ownerName })
+      }
+    }
+
     const [qrCodes, total] = await baseQueryBuilder
       .clone()
       .skip(page * pageSize)
@@ -329,6 +346,40 @@ export class QrCodeService {
     }
   }
 
+  async updateQrCode(
+    id: number,
+    maintenanceCode?: string,
+    remark?: string,
+    info?: PersonInfoDto | PetInfoDto | GoodsInfoDto | LinkInfoDto
+  ): Promise<void> {
+    await this.app.dataSource.transaction(async transactionalEntityManager => {
+      const qrCode = await transactionalEntityManager.findOne(QrCode, { where: { id } })
+      if (!qrCode) {
+        throw new Error('二维码不存在')
+      }
+
+      if (maintenanceCode) {
+        qrCode.maintenanceCode = maintenanceCode
+      }
+
+      if (remark !== undefined) {
+        qrCode.remark = remark
+      }
+
+      // 关联信息
+      if (info) {
+        await this.updateQrCodeInfo(transactionalEntityManager, qrCode, info)
+      }
+
+      if (!qrCode.isActivated) {
+        qrCode.isActivated = true
+        qrCode.activatedAt = new Date()
+      }
+
+      await transactionalEntityManager.save(QrCode, qrCode)
+    })
+  }
+
   /**
    * 根据日期获取二维码列表(用于下载)
    */
@@ -475,27 +526,6 @@ export class QrCodeService {
     await this.userQrCodeRepository.save(userQrCode)
   }
 
-  /**
-   * 更新二维码信息
-   */
-  async updateQrCode(qrCode: string, maintenanceCode: string, data: { remark?: string }): Promise<QrCode> {
-    const isValid = await this.verifyMaintenanceCode(qrCode, maintenanceCode)
-    if (!isValid) {
-      throw new Error('维护码错误')
-    }
-
-    const entity = await this.qrCodeRepository.findOne({ where: { qrCode } })
-    if (!entity) {
-      throw new Error('二维码不存在')
-    }
-
-    if (data.remark !== undefined) {
-      entity.remark = data.remark
-    }
-
-    return await this.qrCodeRepository.save(entity)
-  }
-
   async getUserQrCodes(userId: number, page: number = 0, pageSize: number = 20): Promise<PaginationResponse<any>> {
     const [content, total] = await this.userQrCodeRepository
       .createQueryBuilder('userQrCode')
@@ -567,4 +597,51 @@ export class QrCodeService {
       }
     }
   }
+
+  /**
+   * 更新二维码关联信息
+   */
+  private async updateQrCodeInfo(
+    transactionalEntityManager: any,
+    qrCode: QrCode,
+    info: PersonInfoDto | PetInfoDto | GoodsInfoDto | LinkInfoDto
+  ): Promise<void> {
+    switch (qrCode.qrType) {
+      case QrType.PERSON:
+        await this.updateOrCreateInfo(transactionalEntityManager, PersonInfo, qrCode.id, info as PersonInfoDto)
+        break
+      case QrType.PET:
+        await this.updateOrCreateInfo(transactionalEntityManager, PetInfo, qrCode.id, info as PetInfoDto)
+        break
+      case QrType.GOODS:
+        await this.updateOrCreateInfo(transactionalEntityManager, GoodsInfo, qrCode.id, info as GoodsInfoDto)
+        break
+      case QrType.LINK:
+        await this.updateOrCreateInfo(transactionalEntityManager, LinkInfo, qrCode.id, info as LinkInfoDto)
+        break
+    }
+  }
+
+  /**
+   * 更新或创建 Info 方法
+   */
+  private async updateOrCreateInfo<T>(
+    transactionalEntityManager: any,
+    entityClass: new () => T,
+    qrCodeId: number,
+    info: any
+  ): Promise<void> {
+    const repository = transactionalEntityManager.getRepository(entityClass)
+    const existingInfo = await repository.findOne({ where: { qrCodeId } })
+
+    if (existingInfo) {
+      await repository.update((existingInfo as any).id, info)
+    } else {
+      const newInfo = repository.create({
+        ...info,
+        qrCodeId
+      })
+      await repository.save(newInfo)
+    }
+  }
 }