Explorar o código

添加用户二维码绑定和解绑功能,支持查询用户关联二维码列表,更新相关DTO、服务和路由配置以实现新功能。

wuyi hai 2 semanas
pai
achega
d3f91d2791

+ 70 - 1
src/controllers/qr-code.controller.ts

@@ -6,7 +6,10 @@ import {
   QueryQrCodeDto,
   VerifyMaintenanceCodeDto,
   GetQrCodeInfoDto,
-  ResetMaintenanceCodeDto
+  ResetMaintenanceCodeDto,
+  AddUserQrCodeDto,
+  DeleteUserQrCodeDto,
+  QueryUserQrCodeDto
 } from '../dto/qr-code.dto'
 
 export class QrCodeController {
@@ -230,4 +233,70 @@ export class QrCodeController {
       return reply.code(500).send({ message })
     }
   }
+
+  /**
+   * 绑定二维码
+   */
+  async bindQrCode(request: FastifyRequest<{ Body: AddUserQrCodeDto }>, reply: FastifyReply) {
+    try {
+      const userId = request.user.id
+      const { qrCodeId, qrCode, maintenanceCode } = request.body
+
+      const userQrCode = await this.qrCodeService.bindQrCode(userId, qrCodeId, qrCode, maintenanceCode)
+
+      return reply.code(201).send({
+        message: 'Bound successfully'
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : 'Binding failed'
+      const clientErrorKeywords = [
+        'not found',
+        'already bound',
+        'Please provide',
+        'Maintenance code cannot be empty',
+        'Maintenance code error'
+      ]
+      const isClientError = clientErrorKeywords.some(keyword => message.includes(keyword))
+      const statusCode = isClientError ? 400 : 500
+      return reply.code(statusCode).send({ message })
+    }
+  }
+
+  /**
+   * 取消绑定二维码
+   */
+  async unbindQrCode(request: FastifyRequest<{ Body: DeleteUserQrCodeDto }>, reply: FastifyReply) {
+    try {
+      const userId = request.user.id
+      const { id, qrCodeId } = request.body
+
+      await this.qrCodeService.unbindQrCode(userId, id, qrCodeId)
+
+      return reply.send({
+        message: 'Unbound successfully'
+      })
+    } 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
+      return reply.code(statusCode).send({ message })
+    }
+  }
+
+  /**
+   * 查询用户的关联二维码列表
+   */
+  async getUserQrCodes(request: FastifyRequest<{ Querystring: QueryUserQrCodeDto }>, reply: FastifyReply) {
+    try {
+      const userId = request.user.id
+      const { page = 0, pageSize = 20 } = request.query
+
+      const result = await this.qrCodeService.getUserQrCodes(userId, page, pageSize)
+
+      return reply.send(result)
+    } catch (error) {
+      const message = error instanceof Error ? error.message : 'Query failed'
+      return reply.code(500).send({ message })
+    }
+  }
 }

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

@@ -82,3 +82,38 @@ export class ResetMaintenanceCodeDto {
   @Matches(/^[a-zA-Z0-9]+$/, { message: '维护码只能包含字母和数字' })
   maintenanceCode: string
 }
+
+export class AddUserQrCodeDto {
+  @IsOptional()
+  @IsNumber()
+  qrCodeId?: number
+
+  @IsOptional()
+  @IsString()
+  qrCode?: string
+
+  @IsString()
+  maintenanceCode: string
+}
+
+export class DeleteUserQrCodeDto {
+  @IsOptional()
+  @IsNumber()
+  id?: number
+
+  @IsOptional()
+  @IsNumber()
+  qrCodeId?: number
+}
+
+export class QueryUserQrCodeDto {
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  page?: number = 0
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  pageSize?: number = 20
+}

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

@@ -13,6 +13,7 @@ import { PetInfo } from './pet-info.entity'
 import { GoodsInfo } from './goods-info.entity'
 import { ScanRecord } from './scan-record.entity'
 import { LinkInfo } from './link-info.entity'
+import { UserQrCode } from './user-qr-code.entity'
 
 export enum QrType {
   PERSON = 'person',
@@ -72,4 +73,7 @@ export class QrCode {
 
   @OneToMany(() => ScanRecord, scanRecord => scanRecord.qrCode)
   scanRecords: ScanRecord[]
+
+  @OneToMany(() => UserQrCode, userQrCode => userQrCode.qrCode)
+  userQrCodes: UserQrCode[]
 }

+ 45 - 0
src/entities/user-qr-code.entity.ts

@@ -0,0 +1,45 @@
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  CreateDateColumn,
+  UpdateDateColumn,
+  ManyToOne,
+  JoinColumn,
+  Index
+} from 'typeorm'
+import { User } from './user.entity'
+import { QrCode } from './qr-code.entity'
+
+@Entity()
+@Index(['userId', 'isDeleted'])
+@Index(['qrCodeId', 'isDeleted'])
+@Index(['userId', 'qrCodeId'])
+export class UserQrCode {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column()
+  userId: number
+
+  @Column()
+  qrCodeId: number
+
+  @Column({ default: false })
+  isDeleted: boolean
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+
+  @ManyToOne(() => User, user => user.userQrCodes)
+  @JoinColumn({ name: 'userId' })
+  user: User
+
+  @ManyToOne(() => QrCode, qrCode => qrCode.userQrCodes)
+  @JoinColumn({ name: 'qrCodeId' })
+  qrCode: QrCode
+}
+

+ 5 - 1
src/entities/user.entity.ts

@@ -1,4 +1,5 @@
-import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'
+import { UserQrCode } from './user-qr-code.entity'
 
 export enum UserRole {
   ADMIN = 'admin',
@@ -31,4 +32,7 @@ export class User {
 
   @Column({ nullable: true, default: 0 })
   parentId: number
+
+  @OneToMany(() => UserQrCode, userQrCode => userQrCode.user)
+  userQrCodes: UserQrCode[]
 }

+ 24 - 2
src/routes/qr-code.routes.ts

@@ -1,13 +1,16 @@
 import { FastifyInstance } from 'fastify'
 import { QrCodeController } from '../controllers/qr-code.controller'
-import { hasRole } from '../middlewares/auth.middleware'
+import { hasRole, authenticate } from '../middlewares/auth.middleware'
 import { UserRole } from '../entities/user.entity'
 import {
   GenerateQrCodeDto,
   QueryQrCodeDto,
   VerifyMaintenanceCodeDto,
   GetQrCodeInfoDto,
-  ResetMaintenanceCodeDto
+  ResetMaintenanceCodeDto,
+  AddUserQrCodeDto,
+  DeleteUserQrCodeDto,
+  QueryUserQrCodeDto
 } from '../dto/qr-code.dto'
 
 export default async function qrCodeRoutes(fastify: FastifyInstance) {
@@ -60,4 +63,23 @@ export default async function qrCodeRoutes(fastify: FastifyInstance) {
     { onRequest: [hasRole(UserRole.ADMIN)] },
     qrCodeController.resetMaintenanceCode.bind(qrCodeController)
   )
+
+  // 用户关联二维码接口
+  fastify.post<{ Body: AddUserQrCodeDto }>(
+    '/bind',
+    { onRequest: [authenticate] },
+    qrCodeController.bindQrCode.bind(qrCodeController)
+  )
+
+  fastify.post<{ Body: DeleteUserQrCodeDto }>(
+    '/unbind',
+    { onRequest: [authenticate] },
+    qrCodeController.unbindQrCode.bind(qrCodeController)
+  )
+
+  fastify.get<{ Querystring: QueryUserQrCodeDto }>(
+    '/my',
+    { onRequest: [authenticate] },
+    qrCodeController.getUserQrCodes.bind(qrCodeController)
+  )
 }

+ 151 - 0
src/services/qr-code.service.ts

@@ -5,6 +5,7 @@ import { PersonInfo } from '../entities/person-info.entity'
 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 { PaginationResponse } from '../dto/common.dto'
 import { FileService } from './file.service'
 
@@ -14,6 +15,7 @@ export class QrCodeService {
   private petInfoRepository: Repository<PetInfo>
   private goodsInfoRepository: Repository<GoodsInfo>
   private linkInfoRepository: Repository<LinkInfo>
+  private userQrCodeRepository: Repository<UserQrCode>
   private fileService: FileService
   private app: FastifyInstance
 
@@ -23,6 +25,7 @@ export class QrCodeService {
     this.petInfoRepository = app.dataSource.getRepository(PetInfo)
     this.goodsInfoRepository = app.dataSource.getRepository(GoodsInfo)
     this.linkInfoRepository = app.dataSource.getRepository(LinkInfo)
+    this.userQrCodeRepository = app.dataSource.getRepository(UserQrCode)
     this.fileService = new FileService(app)
     this.app = app
   }
@@ -331,4 +334,152 @@ export class QrCodeService {
 
     return newMaintenanceCode
   }
+
+  async bindQrCode(userId: number, qrCodeId?: number, qrCode?: string, maintenanceCode?: string): Promise<UserQrCode> {
+    let targetQrCodeId: number
+    let targetQrCode: string
+
+    if (qrCodeId) {
+      const qrCodeEntity = await this.qrCodeRepository.findOne({ where: { id: qrCodeId } })
+      if (!qrCodeEntity) {
+        throw new Error('QR code not found')
+      }
+      targetQrCodeId = qrCodeId
+      targetQrCode = qrCodeEntity.qrCode
+    } else if (qrCode) {
+      const qrCodeEntity = await this.qrCodeRepository.findOne({ where: { qrCode } })
+      if (!qrCodeEntity) {
+        throw new Error('QR code not found')
+      }
+      targetQrCodeId = qrCodeEntity.id
+      targetQrCode = qrCode
+    } else {
+      throw new Error('Please provide qrCode')
+    }
+
+    if (!maintenanceCode) {
+      throw new Error('Maintenance code cannot be empty')
+    }
+
+    const isValid = await this.verifyMaintenanceCode(targetQrCode, maintenanceCode)
+    if (!isValid) {
+      throw new Error('Maintenance code error')
+    }
+
+    const existing = await this.userQrCodeRepository.findOne({
+      where: { userId, qrCodeId: targetQrCodeId }
+    })
+
+    if (existing) {
+      if (existing.isDeleted) {
+        existing.isDeleted = false
+        return await this.userQrCodeRepository.save(existing)
+      } else {
+        throw new Error('QR code already bound')
+      }
+    }
+
+    const userQrCode = this.userQrCodeRepository.create({
+      userId,
+      qrCodeId: targetQrCodeId,
+      isDeleted: false
+    })
+
+    return await this.userQrCodeRepository.save(userQrCode)
+  }
+
+  async unbindQrCode(userId: number, id?: number, qrCodeId?: number): Promise<void> {
+    let userQrCode: UserQrCode | null = null
+
+    if (id) {
+      userQrCode = await this.userQrCodeRepository.findOne({ where: { id, userId } })
+    } else if (qrCodeId) {
+      userQrCode = await this.userQrCodeRepository.findOne({ where: { qrCodeId, userId } })
+    } else {
+      throw new Error('Please provide qrCode')
+    }
+
+    if (!userQrCode) {
+      throw new Error('Binding record not found')
+    }
+
+    if (userQrCode.isDeleted) {
+      throw new Error('Binding record already deleted')
+    }
+
+    userQrCode.isDeleted = true
+    await this.userQrCodeRepository.save(userQrCode)
+  }
+
+  async getUserQrCodes(userId: number, page: number = 0, pageSize: number = 20): Promise<PaginationResponse<any>> {
+    const [content, total] = await this.userQrCodeRepository
+      .createQueryBuilder('userQrCode')
+      .leftJoinAndSelect('userQrCode.qrCode', 'qrCode')
+      .leftJoinAndSelect('qrCode.personInfo', 'personInfo')
+      .leftJoinAndSelect('qrCode.petInfo', 'petInfo')
+      .leftJoinAndSelect('qrCode.goodsInfo', 'goodsInfo')
+      .leftJoinAndSelect('qrCode.linkInfo', 'linkInfo')
+      .where('userQrCode.userId = :userId', { userId })
+      .andWhere('userQrCode.isDeleted = :isDeleted', { isDeleted: false })
+      .skip(page * pageSize)
+      .take(pageSize)
+      .orderBy('userQrCode.createdAt', 'DESC')
+      .getManyAndCount()
+
+    const formattedContent = content.map(item => {
+      const qrCode = item.qrCode
+      const qrType = qrCode?.qrType
+
+      let info: any = null
+      let isVisible: boolean = true
+      let name: string | undefined = undefined
+      let jumpUrl: string | undefined = undefined
+
+      if (qrType === QrType.PERSON && qrCode?.personInfo) {
+        info = qrCode.personInfo
+        isVisible = info.isVisible ?? true
+        name = info.name
+      } else if (qrType === QrType.PET && qrCode?.petInfo) {
+        info = qrCode.petInfo
+        isVisible = info.isVisible ?? true
+        name = info.name
+      } else if (qrType === QrType.GOODS && qrCode?.goodsInfo) {
+        info = qrCode.goodsInfo
+        isVisible = info.isVisible ?? true
+        name = info.name
+      } else if (qrType === QrType.LINK && qrCode?.linkInfo) {
+        info = qrCode.linkInfo
+        isVisible = info.isVisible ?? true
+        jumpUrl = info.jumpUrl
+      }
+
+      const result: any = {
+        id: item.id,
+        qrCodeId: item.qrCodeId,
+        qrCode: qrCode?.qrCode,
+        qrType: qrType,
+        isActivated: qrCode?.isActivated,
+        lastScanAt: qrCode?.lastScanAt,
+        isVisible: isVisible
+      }
+
+      // 根据类型添加特定字段
+      if (qrType === QrType.LINK) {
+        result.jumpUrl = jumpUrl
+      } else if (qrType === QrType.PERSON || qrType === QrType.PET || qrType === QrType.GOODS) {
+        result.name = name
+      }
+
+      return result
+    })
+
+    return {
+      content: formattedContent,
+      metadata: {
+        total: Number(total),
+        page: Number(page),
+        size: Number(pageSize)
+      }
+    }
+  }
 }