2 Комити 4155ed55a0 ... 90907fe536

Аутор SHA1 Порука Датум
  wuyi 90907fe536 优化二维码服务,调整查询逻辑以支持日期范围过滤,并确保二维码只能绑定一个用户,更新相关DTO和服务逻辑以提升功能完整性。 пре 3 дана
  wuyi 842e7776b2 添加更新二维码信息的功能,新增UpdateQrCodeDto以支持二维码及备注的更新,更新相关服务和路由配置。 пре 3 дана

+ 28 - 2
src/controllers/qr-code.controller.ts

@@ -9,7 +9,8 @@ import {
   ResetMaintenanceCodeDto,
   AddUserQrCodeDto,
   DeleteUserQrCodeDto,
-  QueryUserQrCodeDto
+  QueryUserQrCodeDto,
+  UpdateQrCodeDto
 } from '../dto/qr-code.dto'
 
 export class QrCodeController {
@@ -253,7 +254,7 @@ export class QrCodeController {
       const userId = request.user.id
       const { qrCodeId, qrCode, maintenanceCode } = request.body
 
-      const userQrCode = await this.qrCodeService.bindQrCode(userId, qrCodeId, qrCode, maintenanceCode)
+      await this.qrCodeService.bindQrCode(userId, qrCodeId, qrCode, maintenanceCode)
 
       return reply.code(201).send({
         message: 'Bound successfully'
@@ -310,4 +311,29 @@ 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 })
+    }
+  }
 }

+ 13 - 0
src/dto/qr-code.dto.ts

@@ -117,3 +117,16 @@ export class QueryUserQrCodeDto {
   @Min(1)
   pageSize?: number = 20
 }
+
+export class UpdateQrCodeDto {
+  @IsString()
+  qrCode: string
+
+  @IsString()
+  maintenanceCode: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  remark?: string
+}

+ 3 - 0
src/entities/qr-code.entity.ts

@@ -47,6 +47,9 @@ export class QrCode {
   @Column({ default: 0 })
   scanCount: number
 
+  @Column({ nullable: true })
+  remark: string
+
   @Column({ type: 'datetime', nullable: true })
   activatedAt: Date
 

+ 1 - 1
src/entities/user-qr-code.entity.ts

@@ -13,7 +13,7 @@ import { QrCode } from './qr-code.entity'
 
 @Entity()
 @Index(['userId', 'isDeleted'])
-@Index(['qrCodeId', 'isDeleted'])
+@Index(['qrCodeId', 'isDeleted', 'id'])
 @Index(['userId', 'qrCodeId'])
 export class UserQrCode {
   @PrimaryGeneratedColumn()

+ 8 - 1
src/routes/qr-code.routes.ts

@@ -10,7 +10,8 @@ import {
   ResetMaintenanceCodeDto,
   AddUserQrCodeDto,
   DeleteUserQrCodeDto,
-  QueryUserQrCodeDto
+  QueryUserQrCodeDto,
+  UpdateQrCodeDto
 } from '../dto/qr-code.dto'
 
 export default async function qrCodeRoutes(fastify: FastifyInstance) {
@@ -82,4 +83,10 @@ 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)
+  )
 }

+ 91 - 17
src/services/qr-code.service.ts

@@ -6,6 +6,7 @@ import { PetInfo } from '../entities/pet-info.entity'
 import { GoodsInfo } from '../entities/goods-info.entity'
 import { LinkInfo } from '../entities/link-info.entity'
 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'
 
@@ -203,7 +204,8 @@ export class QrCodeService {
         createdAt: entity.createdAt,
         updatedAt: entity.updatedAt,
         activatedAt: entity.activatedAt,
-        lastScanAt: entity.lastScanAt
+        lastScanAt: entity.lastScanAt,
+        remark: entity.remark
       }),
       isActivated: entity.isActivated,
       isVisible,
@@ -247,44 +249,82 @@ export class QrCodeService {
     endDate?: string,
     page: number = 0,
     pageSize: number = 20
-  ): Promise<PaginationResponse<QrCode>> {
-    const queryBuilder = this.qrCodeRepository.createQueryBuilder('qrCode')
+  ): Promise<PaginationResponse<any>> {
+    const baseQueryBuilder = this.qrCodeRepository.createQueryBuilder('qrCode')
 
     if (qrCode) {
-      queryBuilder.andWhere('qrCode.qrCode = :qrCode', { qrCode })
+      baseQueryBuilder.andWhere('qrCode.qrCode = :qrCode', { qrCode })
     }
 
     if (qrType) {
-      queryBuilder.andWhere('qrCode.qrType = :qrType', { qrType })
+      baseQueryBuilder.andWhere('qrCode.qrType = :qrType', { qrType })
     }
 
     if (isActivated !== undefined) {
       const isActivatedBool =
         typeof isActivated === 'string' ? isActivated === 'true' || isActivated === '1' : Boolean(isActivated)
-      queryBuilder.andWhere('qrCode.isActivated = :isActivated', { isActivated: isActivatedBool })
+      baseQueryBuilder.andWhere('qrCode.isActivated = :isActivated', { isActivated: isActivatedBool })
     }
 
     if (startDate && endDate) {
-      queryBuilder.andWhere('DATE(qrCode.createdAt) BETWEEN :startDate AND :endDate', { startDate, endDate })
+      const start = `${startDate} 00:00:00`
+      const end = `${endDate} 23:59:59`
+      baseQueryBuilder.andWhere('qrCode.createdAt BETWEEN :start AND :end', { start, end })
     } else if (startDate) {
-      queryBuilder.andWhere('DATE(qrCode.createdAt) >= :startDate', { startDate })
+      const start = `${startDate} 00:00:00`
+      baseQueryBuilder.andWhere('qrCode.createdAt >= :start', { start })
     } else if (endDate) {
-      queryBuilder.andWhere('DATE(qrCode.createdAt) <= :endDate', { endDate })
+      const end = `${endDate} 23:59:59`
+      baseQueryBuilder.andWhere('qrCode.createdAt <= :end', { end })
     }
 
-    const [content, total] = await queryBuilder
-      .select()
+    const [qrCodes, total] = await baseQueryBuilder
+      .clone()
       .skip(page * pageSize)
       .take(pageSize)
       .orderBy('qrCode.createdAt', 'DESC')
       .getManyAndCount()
 
+    const qrCodeIds = qrCodes.map(item => item.id).filter(Boolean)
+
+    const ownerByQrCodeId = new Map<number, { ownerId: number; ownerName: string }>()
+
+    if (qrCodeIds.length > 0) {
+      const rows = await this.userQrCodeRepository
+        .createQueryBuilder('uq')
+        .leftJoin(User, 'owner', 'owner.id = uq.userId')
+        .select('uq.qrCodeId', 'qrCodeId')
+        .addSelect('owner.id', 'ownerId')
+        .addSelect('owner.name', 'ownerName')
+        .where('uq.isDeleted = :isDeleted', { isDeleted: false })
+        .andWhere('uq.qrCodeId IN (:...qrCodeIds)', { qrCodeIds })
+        .orderBy('uq.qrCodeId', 'ASC')
+        .addOrderBy('uq.id', 'DESC')
+        .getRawMany<{ qrCodeId: number; ownerId: number; ownerName: string }>()
+
+      for (const row of rows) {
+        const id = Number((row as any).qrCodeId)
+        if (!ownerByQrCodeId.has(id) && (row as any).ownerId != null) {
+          ownerByQrCodeId.set(id, { ownerId: Number((row as any).ownerId), ownerName: (row as any).ownerName })
+        }
+      }
+    }
+
+    const content = qrCodes.map(entity => {
+      const owner = ownerByQrCodeId.get(entity.id)
+      return {
+        ...entity,
+        ownerId: owner?.ownerId ?? null,
+        ownerName: owner?.ownerName ?? null
+      }
+    })
+
     return {
       content,
       metadata: {
-        total: Number(total),
-        page: Number(page),
-        size: Number(pageSize)
+        total: total,
+        page: page,
+        size: pageSize
       }
     }
   }
@@ -293,9 +333,11 @@ export class QrCodeService {
    * 根据日期获取二维码列表(用于下载)
    */
   async getQrCodesByDate(date: string): Promise<QrCode[]> {
+    const start = `${date} 00:00:00`
+    const end = `${date} 23:59:59`
     return this.qrCodeRepository
       .createQueryBuilder('qrCode')
-      .where('DATE(qrCode.createdAt) = :date', { date })
+      .where('qrCode.createdAt BETWEEN :start AND :end', { start, end })
       .orderBy('qrCode.createdAt', 'ASC')
       .getMany()
   }
@@ -377,6 +419,17 @@ export class QrCodeService {
       throw new Error('Maintenance code error')
     }
 
+    // 一个二维码只能绑定一个用户
+    const activeBind = await this.userQrCodeRepository.findOne({
+      where: { qrCodeId: targetQrCodeId, isDeleted: false }
+    })
+    if (activeBind) {
+      if (activeBind.userId !== userId) {
+        throw new Error('QR code already bound')
+      }
+      return activeBind
+    }
+
     const existing = await this.userQrCodeRepository.findOne({
       where: { userId, qrCodeId: targetQrCodeId }
     })
@@ -422,6 +475,27 @@ 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')
@@ -471,10 +545,10 @@ export class QrCodeService {
         qrType: qrType,
         isActivated: qrCode?.isActivated,
         lastScanAt: qrCode?.lastScanAt,
-        isVisible: isVisible
+        isVisible: isVisible,
+        remark: qrCode?.remark
       }
 
-      // 根据类型添加特定字段
       if (qrType === QrType.LINK) {
         result.jumpUrl = jumpUrl
       } else if (qrType === QrType.PERSON || qrType === QrType.PET || qrType === QrType.GOODS) {