瀏覽代碼

二维码相关 api

wuyi 1 月之前
父節點
當前提交
314ae72757

+ 11 - 3
src/app.ts

@@ -11,12 +11,16 @@ import { createDataSource } from './config/database'
 import userRoutes from './routes/user.routes'
 import fileRoutes from './routes/file.routes'
 import sysConfigRoutes from './routes/sys-config.routes'
+import qrCodeRoutes from './routes/qr-code.routes'
+import personInfoRoutes from './routes/person-info.routes'
+import petInfoRoutes from './routes/pet-info.routes'
+import scanRecordRoutes from './routes/scan-record.routes'
 
 const options: FastifyEnvOptions = {
   schema: schema,
   dotenv: {
-    debug: false,
-  },
+    debug: false
+  }
 }
 
 export const createApp = async () => {
@@ -38,7 +42,7 @@ export const createApp = async () => {
 
   app.register(cors, {
     origin: true,
-    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
+    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
   })
 
   app.register(jwt, {
@@ -77,6 +81,10 @@ export const createApp = async () => {
   app.register(userRoutes, { prefix: '/api/users' })
   app.register(fileRoutes, { prefix: '/api/files' })
   app.register(sysConfigRoutes, { prefix: '/api/sys-config' })
+  app.register(qrCodeRoutes, { prefix: '/api/qr' })
+  app.register(personInfoRoutes, { prefix: '/api/person' })
+  app.register(petInfoRoutes, { prefix: '/api/pet' })
+  app.register(scanRecordRoutes, { prefix: '/api/scan' })
 
   const dataSource = createDataSource(app)
   await dataSource.initialize()

+ 105 - 0
src/controllers/person-info.controller.ts

@@ -0,0 +1,105 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { PersonInfoService } from '../services/person-info.service'
+import { ScanRecordService } from '../services/scan-record.service'
+import {
+  CreatePersonInfoDto,
+  UpdatePersonInfoDto,
+  QueryPersonInfoDto,
+  AdminUpdatePersonInfoDto
+} from '../dto/person-info.dto'
+
+export class PersonInfoController {
+  private personInfoService: PersonInfoService
+  private scanRecordService: ScanRecordService
+
+  constructor(app: FastifyInstance) {
+    this.personInfoService = new PersonInfoService(app)
+    this.scanRecordService = new ScanRecordService(app)
+  }
+
+  async create(request: FastifyRequest<{ Body: CreatePersonInfoDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode, ...data } = request.body
+
+      const personInfo = await this.personInfoService.create(qrCode, data)
+
+      return reply.code(201).send({
+        message: '人员信息创建成功',
+        data: personInfo
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '创建失败'
+      return reply.code(400).send({ message })
+    }
+  }
+
+  async update(request: FastifyRequest<{ Body: UpdatePersonInfoDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode, maintenanceCode, ...data } = request.body
+
+      const personInfo = await this.personInfoService.update(qrCode, maintenanceCode, data)
+
+      return reply.send({
+        message: '人员信息更新成功',
+        data: personInfo
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '更新失败'
+      return reply.code(400).send({ message })
+    }
+  }
+
+  async get(request: FastifyRequest<{ Querystring: { qrCode: string } }>, reply: FastifyReply) {
+    try {
+      const { qrCode } = request.query
+
+      if (!qrCode) {
+        return reply.code(400).send({ message: '请提供二维码参数' })
+      }
+
+      const personInfo = await this.personInfoService.findByQrCode(qrCode)
+
+      if (!personInfo) {
+        return reply.code(404).send({ message: '人员信息不存在' })
+      }
+
+      return reply.send(personInfo)
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '获取失败'
+      return reply.code(500).send({ message })
+    }
+  }
+
+  async adminUpdate(request: FastifyRequest<{ Body: AdminUpdatePersonInfoDto }>, reply: FastifyReply) {
+    try {
+      const { qrCodeId, ...data } = request.body
+
+      if (!qrCodeId) {
+        return reply.code(400).send({ message: '请提供二维码ID' })
+      }
+
+      const personInfo = await this.personInfoService.adminUpdate(qrCodeId, data)
+
+      return reply.send({
+        message: '人员信息更新成功',
+        data: personInfo
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '更新失败'
+      return reply.code(400).send({ message })
+    }
+  }
+
+  async list(request: FastifyRequest<{ Querystring: QueryPersonInfoDto }>, reply: FastifyReply) {
+    try {
+      const { name, phone, gender, startDate, endDate, page, pageSize } = request.query
+
+      const result = await this.personInfoService.list(name, phone, gender, startDate, endDate, page, pageSize)
+
+      return reply.send(result)
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '查询失败'
+      return reply.code(500).send({ message })
+    }
+  }
+}

+ 108 - 0
src/controllers/pet-info.controller.ts

@@ -0,0 +1,108 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { PetInfoService } from '../services/pet-info.service'
+import { ScanRecordService } from '../services/scan-record.service'
+import { CreatePetInfoDto, UpdatePetInfoDto, QueryPetInfoDto, AdminUpdatePetInfoDto } from '../dto/pet-info.dto'
+
+export class PetInfoController {
+  private petInfoService: PetInfoService
+  private scanRecordService: ScanRecordService
+
+  constructor(app: FastifyInstance) {
+    this.petInfoService = new PetInfoService(app)
+    this.scanRecordService = new ScanRecordService(app)
+  }
+
+  async create(request: FastifyRequest<{ Body: CreatePetInfoDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode, ...data } = request.body
+
+      const petInfo = await this.petInfoService.create(qrCode, data)
+
+      return reply.code(201).send({
+        message: '宠物/物品信息创建成功',
+        data: petInfo
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '创建失败'
+      return reply.code(400).send({ message })
+    }
+  }
+
+  async update(request: FastifyRequest<{ Body: UpdatePetInfoDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode, maintenanceCode, ...data } = request.body
+
+      const petInfo = await this.petInfoService.update(qrCode, maintenanceCode, data)
+
+      return reply.send({
+        message: '宠物/物品信息更新成功',
+        data: petInfo
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '更新失败'
+      return reply.code(400).send({ message })
+    }
+  }
+
+  async get(request: FastifyRequest<{ Querystring: { qrCode: string } }>, reply: FastifyReply) {
+    try {
+      const { qrCode } = request.query
+
+      if (!qrCode) {
+        return reply.code(400).send({ message: '请提供二维码参数' })
+      }
+
+      const petInfo = await this.petInfoService.findByQrCode(qrCode)
+
+      if (!petInfo) {
+        return reply.code(404).send({ message: '宠物/物品信息不存在' })
+      }
+
+      return reply.send(petInfo)
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '获取失败'
+      return reply.code(500).send({ message })
+    }
+  }
+
+  async adminUpdate(request: FastifyRequest<{ Body: AdminUpdatePetInfoDto }>, reply: FastifyReply) {
+    try {
+      const { qrCodeId, ...data } = request.body
+
+      if (!qrCodeId) {
+        return reply.code(400).send({ message: '请提供二维码ID' })
+      }
+
+      const petInfo = await this.petInfoService.adminUpdate(qrCodeId, data)
+
+      return reply.send({
+        message: '宠物/物品信息更新成功',
+        data: petInfo
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '更新失败'
+      return reply.code(400).send({ message })
+    }
+  }
+
+  async list(request: FastifyRequest<{ Querystring: QueryPetInfoDto }>, reply: FastifyReply) {
+    try {
+      const { name, contactName, contactPhone, startDate, endDate, page, pageSize } = request.query
+
+      const result = await this.petInfoService.query(
+        name,
+        contactName,
+        contactPhone,
+        startDate,
+        endDate,
+        page,
+        pageSize
+      )
+
+      return reply.send(result)
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '查询失败'
+      return reply.code(500).send({ message })
+    }
+  }
+}

+ 147 - 0
src/controllers/qr-code.controller.ts

@@ -0,0 +1,147 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { QrCodeService } from '../services/qr-code.service'
+import { ScanRecordService } from '../services/scan-record.service'
+import { GenerateQrCodeDto, QueryQrCodeDto, VerifyMaintenanceCodeDto, GetQrCodeInfoDto } from '../dto/qr-code.dto'
+
+export class QrCodeController {
+  private qrCodeService: QrCodeService
+  private scanRecordService: ScanRecordService
+
+  constructor(app: FastifyInstance) {
+    this.qrCodeService = new QrCodeService(app)
+    this.scanRecordService = new ScanRecordService(app)
+  }
+
+  /**
+   * 生成二维码
+   */
+  async generate(request: FastifyRequest<{ Body: GenerateQrCodeDto }>, reply: FastifyReply) {
+    try {
+      const { qrType, quantity } = request.body
+
+      const result = await this.qrCodeService.generateQrCodes(qrType, quantity || 1)
+
+      return reply.code(201).send({
+        message: '二维码生成成功',
+        data: result
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '生成失败'
+      return reply.code(500).send({ message })
+    }
+  }
+
+  /**
+   * 分页列表
+   */
+  async list(request: FastifyRequest<{ Querystring: QueryQrCodeDto }>, reply: FastifyReply) {
+    try {
+      const { qrType, isActivated, startDate, endDate, page, pageSize } = request.query
+
+      const result = await this.qrCodeService.queryQrCodes(qrType, isActivated, startDate, endDate, page, pageSize)
+
+      return reply.send(result)
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '查询失败'
+      return reply.code(500).send({ message })
+    }
+  }
+
+  /**
+   * 按日期下载
+   */
+  async downloadByDate(request: FastifyRequest<{ Querystring: { date: string } }>, reply: FastifyReply) {
+    try {
+      const { date } = request.query
+
+      if (!date) {
+        return reply.code(400).send({ message: '请提供日期参数' })
+      }
+
+      const qrCodes = await this.qrCodeService.getQrCodesByDate(date)
+
+      return reply.send({
+        date,
+        count: qrCodes.length,
+        data: qrCodes
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '下载失败'
+      return reply.code(500).send({ message })
+    }
+  }
+
+  /**
+   * 验证维护码
+   */
+  async verifyMaintenanceCode(request: FastifyRequest<{ Body: VerifyMaintenanceCodeDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode, maintenanceCode } = request.body
+
+      const isValid = await this.qrCodeService.verifyMaintenanceCode(qrCode, maintenanceCode)
+
+      if (!isValid) {
+        return reply.code(401).send({ message: '维护码错误' })
+      }
+
+      return reply.send({ message: '验证成功', valid: true })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '验证失败'
+      return reply.code(500).send({ message })
+    }
+  }
+
+  /**
+   * 获取二维码信息
+   */
+  async getInfo(request: FastifyRequest<{ Querystring: GetQrCodeInfoDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode } = request.query
+
+      if (!qrCode) {
+        return reply.code(400).send({ message: '请提供二维码参数' })
+      }
+
+      // 记录扫描
+      const ipAddress = request.ip
+      const userAgent = request.headers['user-agent']
+
+      await this.scanRecordService.create(qrCode, undefined, undefined, undefined, ipAddress, userAgent)
+
+      // 获取二维码信息
+      const info = await this.qrCodeService.getQrCodeInfo(qrCode)
+
+      return reply.send(info)
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '获取信息失败'
+      return reply.code(500).send({ message })
+    }
+  }
+
+  /**
+   * 获取扫描记录
+   */
+  async getScanRecords(
+    request: FastifyRequest<{ Querystring: { qrCode: string; limit?: number } }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { qrCode, limit } = request.query
+
+      if (!qrCode) {
+        return reply.code(400).send({ message: '请提供二维码参数' })
+      }
+
+      const records = await this.scanRecordService.getRecentRecords(qrCode, limit || 10)
+
+      return reply.send({
+        qrCode,
+        count: records.length,
+        records
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '获取记录失败'
+      return reply.code(500).send({ message })
+    }
+  }
+}

+ 77 - 0
src/controllers/scan-record.controller.ts

@@ -0,0 +1,77 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { ScanRecordService } from '../services/scan-record.service'
+import { CreateScanRecordDto, QueryScanRecordDto, QueryScanRecordWithPaginationDto } from '../dto/scan-record.dto'
+
+export class ScanRecordController {
+  private scanRecordService: ScanRecordService
+
+  constructor(app: FastifyInstance) {
+    this.scanRecordService = new ScanRecordService(app)
+  }
+
+  /**
+   * 创建扫描记录(前台自动调用)
+   */
+  async create(request: FastifyRequest<{ Body: CreateScanRecordDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode, latitude, longitude, address, ipAddress, userAgent } = request.body
+
+      // 如果前端没有提供IP和UserAgent,从请求中获取
+      const finalIpAddress = ipAddress || request.ip
+      const finalUserAgent = userAgent || request.headers['user-agent']
+
+      const scanRecord = await this.scanRecordService.create(
+        qrCode,
+        latitude,
+        longitude,
+        address,
+        finalIpAddress,
+        finalUserAgent
+      )
+
+      return reply.code(201).send({
+        message: '扫描记录创建成功',
+        data: scanRecord
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '创建失败'
+      return reply.code(400).send({ message })
+    }
+  }
+
+  /**
+   * 获取最近扫描记录
+   */
+  async getRecent(request: FastifyRequest<{ Querystring: QueryScanRecordDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode, limit } = request.query
+
+      const records = await this.scanRecordService.getRecentRecords(qrCode, limit || 10)
+
+      return reply.send({
+        qrCode,
+        count: records.length,
+        records
+      })
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '获取失败'
+      return reply.code(500).send({ message })
+    }
+  }
+
+  /**
+   * 条件分页查询扫描记录
+   */
+  async list(request: FastifyRequest<{ Querystring: QueryScanRecordWithPaginationDto }>, reply: FastifyReply) {
+    try {
+      const { qrCode, startDate, endDate, page, pageSize } = request.query
+
+      const result = await this.scanRecordService.queryRecords(qrCode, startDate, endDate, page, pageSize)
+
+      return reply.send(result)
+    } catch (error) {
+      const message = error instanceof Error ? error.message : '查询失败'
+      return reply.code(500).send({ message })
+    }
+  }
+}

+ 162 - 0
src/dto/person-info.dto.ts

@@ -0,0 +1,162 @@
+import { IsString, IsEnum, IsOptional, IsEmail, IsPhoneNumber, Length, MaxLength, IsNumber, IsDateString, Min } from 'class-validator'
+import { Gender } from '../entities/person-info.entity'
+
+export class CreatePersonInfoDto {
+  @IsString()
+  qrCode: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  photoUrl?: string
+
+  @IsString()
+  @Length(1, 100)
+  name: string
+
+  @IsEnum(Gender)
+  gender: Gender
+
+  @IsString()
+  @Length(1, 20)
+  phone: string
+
+  @IsOptional()
+  @IsString()
+  specialNote?: string
+
+  @IsString()
+  @Length(1, 100)
+  emergencyContactName: string
+
+  @IsString()
+  @Length(1, 20)
+  emergencyContactPhone: string
+
+  @IsOptional()
+  @IsEmail()
+  @MaxLength(100)
+  emergencyContactEmail?: string
+}
+
+export class UpdatePersonInfoDto {
+  @IsString()
+  qrCode: string
+
+  @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
+}
+
+export class QueryPersonInfoDto {
+  @IsOptional()
+  @IsString()
+  name?: string
+
+  @IsOptional()
+  @IsString()
+  phone?: string
+
+  @IsOptional()
+  @IsEnum(Gender)
+  gender?: Gender
+
+  @IsOptional()
+  @IsDateString()
+  startDate?: string
+
+  @IsOptional()
+  @IsDateString()
+  endDate?: string
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  page?: number = 1
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  pageSize?: number = 20
+}
+
+export class AdminUpdatePersonInfoDto {
+  @IsNumber()
+  qrCodeId: number
+
+  @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
+}
+

+ 124 - 0
src/dto/pet-info.dto.ts

@@ -0,0 +1,124 @@
+import { IsString, IsOptional, IsEmail, Length, MaxLength, IsNumber, IsDateString, Min } from 'class-validator'
+
+export class CreatePetInfoDto {
+  @IsString()
+  qrCode: string
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(500)
+  photoUrl?: string
+
+  @IsString()
+  @Length(1, 100)
+  name: string
+
+  @IsString()
+  @Length(1, 100)
+  contactName: string
+
+  @IsString()
+  @Length(1, 20)
+  contactPhone: string
+
+  @IsOptional()
+  @IsEmail()
+  @MaxLength(100)
+  contactEmail?: string
+}
+
+export class UpdatePetInfoDto {
+  @IsString()
+  qrCode: string
+
+  @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
+}
+
+export class QueryPetInfoDto {
+  @IsOptional()
+  @IsString()
+  name?: string
+
+  @IsOptional()
+  @IsString()
+  contactName?: string
+
+  @IsOptional()
+  @IsString()
+  contactPhone?: string
+
+  @IsOptional()
+  @IsDateString()
+  startDate?: string
+
+  @IsOptional()
+  @IsDateString()
+  endDate?: string
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  page?: number = 1
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  pageSize?: number = 20
+}
+
+export class AdminUpdatePetInfoDto {
+  @IsNumber()
+  qrCodeId: number
+
+  @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
+}
+

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

@@ -0,0 +1,53 @@
+import { IsString, IsEnum, IsOptional, IsNumber, IsBoolean, IsDateString, Min } from 'class-validator'
+import { QrType } from '../entities/qr-code.entity'
+
+export class GenerateQrCodeDto {
+  @IsEnum(QrType)
+  qrType: QrType
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  quantity?: number = 1
+}
+
+export class QueryQrCodeDto {
+  @IsOptional()
+  @IsEnum(QrType)
+  qrType?: QrType
+
+  @IsOptional()
+  @IsBoolean()
+  isActivated?: boolean
+
+  @IsOptional()
+  @IsDateString()
+  startDate?: string
+
+  @IsOptional()
+  @IsDateString()
+  endDate?: string
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  page?: number = 1
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  pageSize?: number = 20
+}
+
+export class VerifyMaintenanceCodeDto {
+  @IsString()
+  qrCode: string
+
+  @IsString()
+  maintenanceCode: string
+}
+
+export class GetQrCodeInfoDto {
+  @IsString()
+  qrCode: string
+}

+ 65 - 0
src/dto/scan-record.dto.ts

@@ -0,0 +1,65 @@
+import { IsString, IsOptional, IsNumber, IsDateString, Min, Max } from 'class-validator'
+
+export class CreateScanRecordDto {
+  @IsString()
+  qrCode: string
+
+  @IsOptional()
+  @IsNumber()
+  @Min(-90)
+  @Max(90)
+  latitude?: number
+
+  @IsOptional()
+  @IsNumber()
+  @Min(-180)
+  @Max(180)
+  longitude?: number
+
+  @IsOptional()
+  @IsString()
+  address?: string
+
+  @IsOptional()
+  @IsString()
+  ipAddress?: string
+
+  @IsOptional()
+  @IsString()
+  userAgent?: string
+}
+
+export class QueryScanRecordDto {
+  @IsString()
+  qrCode: string
+
+  @IsOptional()
+  @IsNumber()
+  limit?: number = 10
+}
+
+export class QueryScanRecordWithPaginationDto {
+  @IsOptional()
+  @IsString()
+  qrCode?: string
+
+  @IsOptional()
+  @IsDateString()
+  startDate?: string
+
+  @IsOptional()
+  @IsDateString()
+  endDate?: string
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  page?: number = 1
+
+  @IsOptional()
+  @IsNumber()
+  @Min(1)
+  @Max(100)
+  pageSize?: number = 20
+}
+

+ 62 - 0
src/entities/person-info.entity.ts

@@ -0,0 +1,62 @@
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  CreateDateColumn,
+  UpdateDateColumn,
+  OneToOne,
+  JoinColumn
+} from 'typeorm'
+import { QrCode } from './qr-code.entity'
+
+export enum Gender {
+  MALE = 'male',
+  FEMALE = 'female',
+  OTHER = 'other'
+}
+
+@Entity()
+export class PersonInfo {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column({ unique: true })
+  qrCodeId: number
+
+  @Column({ nullable: true, length: 500 })
+  photoUrl: string
+
+  @Column({ length: 100 })
+  name: string
+
+  @Column({
+    type: 'enum',
+    enum: Gender
+  })
+  gender: Gender
+
+  @Column({ length: 20 })
+  phone: string
+
+  @Column({ type: 'text', nullable: true })
+  specialNote: string
+
+  @Column({ length: 100 })
+  emergencyContactName: string
+
+  @Column({ length: 20 })
+  emergencyContactPhone: string
+
+  @Column({ length: 100, nullable: true })
+  emergencyContactEmail: string
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+
+  @OneToOne(() => QrCode, qrCode => qrCode.personInfo)
+  @JoinColumn({ name: 'qrCodeId' })
+  qrCode: QrCode
+}

+ 44 - 0
src/entities/pet-info.entity.ts

@@ -0,0 +1,44 @@
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  CreateDateColumn,
+  UpdateDateColumn,
+  OneToOne,
+  JoinColumn
+} from 'typeorm'
+import { QrCode } from './qr-code.entity'
+
+@Entity()
+export class PetInfo {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column({ unique: true })
+  qrCodeId: number
+
+  @Column({ nullable: true, length: 500 })
+  photoUrl: string
+
+  @Column({ length: 100 })
+  name: string
+
+  @Column({ length: 100 })
+  contactName: string
+
+  @Column({ length: 20 })
+  contactPhone: string
+
+  @Column({ length: 100, nullable: true })
+  contactEmail: string
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+
+  @OneToOne(() => QrCode, qrCode => qrCode.petInfo)
+  @JoinColumn({ name: 'qrCodeId' })
+  qrCode: QrCode
+}

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

@@ -0,0 +1,60 @@
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  CreateDateColumn,
+  UpdateDateColumn,
+  Index,
+  OneToOne,
+  OneToMany
+} from 'typeorm'
+import { PersonInfo } from './person-info.entity'
+import { PetInfo } from './pet-info.entity'
+import { ScanRecord } from './scan-record.entity'
+
+export enum QrType {
+  PERSON = 'person',
+  PET = 'pet'
+}
+
+@Entity()
+@Index(['qrType'])
+@Index(['isActivated'])
+@Index(['createdAt'])
+export class QrCode {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column({ unique: true, length: 100 })
+  qrCode: string
+
+  @Column({ length: 100 })
+  maintenanceCode: string
+
+  @Column({
+    type: 'enum',
+    enum: QrType
+  })
+  qrType: QrType
+
+  @Column({ default: false })
+  isActivated: boolean
+
+  @Column({ default: 0 })
+  scanCount: number
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+
+  @OneToOne(() => PersonInfo, personInfo => personInfo.qrCode)
+  personInfo: PersonInfo
+
+  @OneToOne(() => PetInfo, petInfo => petInfo.qrCode)
+  petInfo: PetInfo
+
+  @OneToMany(() => ScanRecord, scanRecord => scanRecord.qrCode)
+  scanRecords: ScanRecord[]
+}

+ 39 - 0
src/entities/scan-record.entity.ts

@@ -0,0 +1,39 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'
+import { QrCode } from './qr-code.entity'
+
+@Entity()
+@Index(['qrCodeId'])
+@Index(['scanTime'])
+@Index(['qrCodeId', 'scanTime'])
+export class ScanRecord {
+  @PrimaryGeneratedColumn('increment', { type: 'bigint' })
+  id: number
+
+  @Column()
+  qrCodeId: number
+
+  @Column({ type: 'datetime' })
+  scanTime: Date
+
+  @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true })
+  latitude: number
+
+  @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true })
+  longitude: number
+
+  @Column({ type: 'text', nullable: true })
+  address: string
+
+  @Column({ length: 50, nullable: true })
+  ipAddress: string
+
+  @Column({ type: 'text', nullable: true })
+  userAgent: string
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @ManyToOne(() => QrCode, qrCode => qrCode.scanRecords)
+  @JoinColumn({ name: 'qrCodeId' })
+  qrCode: QrCode
+}

+ 0 - 5
src/misc/chain.enum.ts

@@ -1,5 +0,0 @@
-export enum Chain {
-  TRON = 'tron',
-  ETH = 'eth',
-  BTC = 'btc'
-}

+ 32 - 0
src/routes/person-info.routes.ts

@@ -0,0 +1,32 @@
+import { FastifyInstance } from 'fastify'
+import { PersonInfoController } from '../controllers/person-info.controller'
+import { hasRole } from '../middlewares/auth.middleware'
+import { UserRole } from '../entities/user.entity'
+import {
+  CreatePersonInfoDto,
+  UpdatePersonInfoDto,
+  QueryPersonInfoDto,
+  AdminUpdatePersonInfoDto
+} from '../dto/person-info.dto'
+
+export default async function personInfoRoutes(fastify: FastifyInstance) {
+  const personInfoController = new PersonInfoController(fastify)
+
+  fastify.post<{ Body: CreatePersonInfoDto }>('/create', personInfoController.create.bind(personInfoController))
+
+  fastify.put<{ Body: UpdatePersonInfoDto }>('/update', personInfoController.update.bind(personInfoController))
+
+  fastify.get<{ Querystring: { qrCode: string } }>('/get', personInfoController.get.bind(personInfoController))
+
+  fastify.get<{ Querystring: QueryPersonInfoDto }>(
+    '/list',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    personInfoController.list.bind(personInfoController)
+  )
+
+  fastify.post<{ Body: AdminUpdatePersonInfoDto }>(
+    '/admin/update',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    personInfoController.adminUpdate.bind(personInfoController)
+  )
+}

+ 27 - 0
src/routes/pet-info.routes.ts

@@ -0,0 +1,27 @@
+import { FastifyInstance } from 'fastify'
+import { PetInfoController } from '../controllers/pet-info.controller'
+import { hasRole } from '../middlewares/auth.middleware'
+import { UserRole } from '../entities/user.entity'
+import { CreatePetInfoDto, UpdatePetInfoDto, QueryPetInfoDto, AdminUpdatePetInfoDto } from '../dto/pet-info.dto'
+
+export default async function petInfoRoutes(fastify: FastifyInstance) {
+  const petInfoController = new PetInfoController(fastify)
+
+  fastify.post<{ Body: CreatePetInfoDto }>('/create', petInfoController.create.bind(petInfoController))
+
+  fastify.put<{ Body: UpdatePetInfoDto }>('/update', petInfoController.update.bind(petInfoController))
+
+  fastify.get<{ Querystring: { qrCode: string } }>('/get', petInfoController.get.bind(petInfoController))
+
+  fastify.get<{ Querystring: QueryPetInfoDto }>(
+    '/list',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    petInfoController.list.bind(petInfoController)
+  )
+
+  fastify.post<{ Body: AdminUpdatePetInfoDto }>(
+    '/admin/update',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    petInfoController.adminUpdate.bind(petInfoController)
+  )
+}

+ 40 - 0
src/routes/qr-code.routes.ts

@@ -0,0 +1,40 @@
+import { FastifyInstance } from 'fastify'
+import { QrCodeController } from '../controllers/qr-code.controller'
+import { hasRole } from '../middlewares/auth.middleware'
+import { UserRole } from '../entities/user.entity'
+import { GenerateQrCodeDto, QueryQrCodeDto, VerifyMaintenanceCodeDto, GetQrCodeInfoDto } from '../dto/qr-code.dto'
+
+export default async function qrCodeRoutes(fastify: FastifyInstance) {
+  const qrCodeController = new QrCodeController(fastify)
+
+  fastify.post<{ Body: GenerateQrCodeDto }>(
+    '/generate',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    qrCodeController.generate.bind(qrCodeController)
+  )
+
+  fastify.get<{ Querystring: QueryQrCodeDto }>(
+    '/list',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    qrCodeController.list.bind(qrCodeController)
+  )
+
+  fastify.get<{ Querystring: { date: string } }>(
+    '/download',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    qrCodeController.downloadByDate.bind(qrCodeController)
+  )
+
+  fastify.get<{ Querystring: { qrCode: string; limit?: number } }>(
+    '/scan',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    qrCodeController.getScanRecords.bind(qrCodeController)
+  )
+
+  fastify.get<{ Querystring: GetQrCodeInfoDto }>('/info', qrCodeController.getInfo.bind(qrCodeController))
+
+  fastify.post<{ Body: VerifyMaintenanceCodeDto }>(
+    '/verify',
+    qrCodeController.verifyMaintenanceCode.bind(qrCodeController)
+  )
+}

+ 19 - 0
src/routes/scan-record.routes.ts

@@ -0,0 +1,19 @@
+import { FastifyInstance } from 'fastify'
+import { ScanRecordController } from '../controllers/scan-record.controller'
+import { CreateScanRecordDto, QueryScanRecordDto, QueryScanRecordWithPaginationDto } from '../dto/scan-record.dto'
+import { hasRole } from '../middlewares/auth.middleware'
+import { UserRole } from '../entities/user.entity'
+
+export default async function scanRecordRoutes(fastify: FastifyInstance) {
+  const scanRecordController = new ScanRecordController(fastify)
+
+  fastify.post<{ Body: CreateScanRecordDto }>('/create', scanRecordController.create.bind(scanRecordController))
+
+  fastify.get<{ Querystring: QueryScanRecordDto }>('/recent', scanRecordController.getRecent.bind(scanRecordController))
+
+  fastify.get<{ Querystring: QueryScanRecordWithPaginationDto }>(
+    '/list',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    scanRecordController.list.bind(scanRecordController)
+  )
+}

+ 147 - 0
src/services/person-info.service.ts

@@ -0,0 +1,147 @@
+import { Repository } from 'typeorm'
+import { FastifyInstance } from 'fastify'
+import { PersonInfo, Gender } from '../entities/person-info.entity'
+import { QrCodeService } from './qr-code.service'
+import { QrType } from '../entities/qr-code.entity'
+import { PaginationResponse } from '../dto/common.dto'
+import { AdminUpdatePersonInfoDto } from '../dto/person-info.dto'
+
+export class PersonInfoService {
+  private personInfoRepository: Repository<PersonInfo>
+  private qrCodeService: QrCodeService
+
+  constructor(app: FastifyInstance) {
+    this.personInfoRepository = app.dataSource.getRepository(PersonInfo)
+    this.qrCodeService = new QrCodeService(app)
+  }
+
+  async create(qrCode: string, data: Partial<PersonInfo>): Promise<PersonInfo> {
+    const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+    if (!qrCodeEntity) {
+      throw new Error('二维码不存在')
+    }
+
+    if (qrCodeEntity.qrType !== QrType.PERSON) {
+      throw new Error('二维码类型不匹配')
+    }
+
+    if (qrCodeEntity.isActivated) {
+      throw new Error('二维码已激活,无法重复填写')
+    }
+
+    const personInfo = this.personInfoRepository.create({
+      ...data,
+      qrCodeId: qrCodeEntity.id
+    })
+
+    const result = await this.personInfoRepository.save(personInfo)
+
+    await this.qrCodeService.activateQrCode(qrCodeEntity.id)
+
+    return result
+  }
+
+  async update(qrCode: string, maintenanceCode: string, data: Partial<PersonInfo>): Promise<PersonInfo> {
+    const isValid = await this.qrCodeService.verifyMaintenanceCode(qrCode, maintenanceCode)
+    if (!isValid) {
+      throw new Error('维护码错误')
+    }
+
+    const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+    if (!qrCodeEntity) {
+      throw new Error('二维码不存在')
+    }
+
+    const personInfo = await this.personInfoRepository.findOne({
+      where: { qrCodeId: qrCodeEntity.id }
+    })
+
+    if (!personInfo) {
+      throw new Error('人员信息不存在')
+    }
+
+    await this.personInfoRepository.update(personInfo.id, data)
+
+    const updated = await this.personInfoRepository.findOne({ where: { id: personInfo.id } })
+    if (!updated) {
+      throw new Error('更新后无法找到人员信息')
+    }
+    return updated
+  }
+
+  async findByQrCodeId(qrCodeId: number): Promise<PersonInfo | null> {
+    return this.personInfoRepository.findOne({ where: { qrCodeId } })
+  }
+
+  async findByQrCode(qrCode: string): Promise<PersonInfo | null> {
+    const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+    if (!qrCodeEntity) {
+      return null
+    }
+    return this.findByQrCodeId(qrCodeEntity.id)
+  }
+
+  async adminUpdate(qrCodeId: number, data: Omit<AdminUpdatePersonInfoDto, 'qrCodeId'>): Promise<PersonInfo> {
+    const personInfo = await this.personInfoRepository.findOne({
+      where: { qrCodeId }
+    })
+
+    if (!personInfo) {
+      throw new Error('人员信息不存在')
+    }
+
+    await this.personInfoRepository.update(personInfo.id, data)
+    const updated = await this.personInfoRepository.findOne({ where: { id: personInfo.id } })
+    if (!updated) {
+      throw new Error('更新后无法找到人员信息')
+    }
+    return updated
+  }
+
+  async list(
+    name?: string,
+    phone?: string,
+    gender?: Gender,
+    startDate?: string,
+    endDate?: string,
+    page: number = 0,
+    pageSize: number = 20
+  ): Promise<PaginationResponse<PersonInfo>> {
+    const queryBuilder = this.personInfoRepository.createQueryBuilder('personInfo')
+
+    if (name) {
+      queryBuilder.andWhere('personInfo.name LIKE :name', { name: `%${name}%` })
+    }
+
+    if (phone) {
+      queryBuilder.andWhere('personInfo.phone LIKE :phone', { phone: `%${phone}%` })
+    }
+
+    if (gender) {
+      queryBuilder.andWhere('personInfo.gender = :gender', { gender })
+    }
+
+    if (startDate && endDate) {
+      queryBuilder.andWhere('DATE(personInfo.createdAt) BETWEEN :startDate AND :endDate', { startDate, endDate })
+    } else if (startDate) {
+      queryBuilder.andWhere('DATE(personInfo.createdAt) >= :startDate', { startDate })
+    } else if (endDate) {
+      queryBuilder.andWhere('DATE(personInfo.createdAt) <= :endDate', { endDate })
+    }
+
+    const [content, total] = await queryBuilder
+      .skip(page * pageSize)
+      .take(pageSize)
+      .orderBy('personInfo.createdAt', 'DESC')
+      .getManyAndCount()
+
+    return {
+      content,
+      metadata: {
+        total: Number(total),
+        page: Number(page),
+        size: Number(pageSize)
+      }
+    }
+  }
+}

+ 147 - 0
src/services/pet-info.service.ts

@@ -0,0 +1,147 @@
+import { Repository } from 'typeorm'
+import { FastifyInstance } from 'fastify'
+import { PetInfo } from '../entities/pet-info.entity'
+import { QrCodeService } from './qr-code.service'
+import { QrType } from '../entities/qr-code.entity'
+import { PaginationResponse } from '../dto/common.dto'
+import { AdminUpdatePetInfoDto } from '../dto/pet-info.dto'
+
+export class PetInfoService {
+  private petInfoRepository: Repository<PetInfo>
+  private qrCodeService: QrCodeService
+
+  constructor(app: FastifyInstance) {
+    this.petInfoRepository = app.dataSource.getRepository(PetInfo)
+    this.qrCodeService = new QrCodeService(app)
+  }
+
+  async create(qrCode: string, data: Partial<PetInfo>): Promise<PetInfo> {
+    const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+    if (!qrCodeEntity) {
+      throw new Error('二维码不存在')
+    }
+
+    if (qrCodeEntity.qrType !== QrType.PET) {
+      throw new Error('二维码类型不匹配')
+    }
+
+    if (qrCodeEntity.isActivated) {
+      throw new Error('二维码已激活,无法重复填写')
+    }
+
+    const petInfo = this.petInfoRepository.create({
+      ...data,
+      qrCodeId: qrCodeEntity.id
+    })
+
+    const result = await this.petInfoRepository.save(petInfo)
+
+    await this.qrCodeService.activateQrCode(qrCodeEntity.id)
+
+    return result
+  }
+
+  async update(qrCode: string, maintenanceCode: string, data: Partial<PetInfo>): Promise<PetInfo | null> {
+    const isValid = await this.qrCodeService.verifyMaintenanceCode(qrCode, maintenanceCode)
+    if (!isValid) {
+      throw new Error('维护码错误')
+    }
+
+    const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+    if (!qrCodeEntity) {
+      throw new Error('二维码不存在')
+    }
+
+    const petInfo = await this.petInfoRepository.findOne({
+      where: { qrCodeId: qrCodeEntity.id }
+    })
+
+    if (!petInfo) {
+      throw new Error('宠物/物品信息不存在')
+    }
+
+    await this.petInfoRepository.update(petInfo.id, data)
+
+    const updated = await this.petInfoRepository.findOne({ where: { id: petInfo.id } })
+    if (!updated) {
+      throw new Error('更新后无法找到宠物/物品信息')
+    }
+    return updated
+  }
+
+  async findByQrCodeId(qrCodeId: number): Promise<PetInfo | null> {
+    return this.petInfoRepository.findOne({ where: { qrCodeId } })
+  }
+
+  async findByQrCode(qrCode: string): Promise<PetInfo | null> {
+    const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+    if (!qrCodeEntity) {
+      return null
+    }
+    return this.findByQrCodeId(qrCodeEntity.id)
+  }
+
+  async adminUpdate(qrCodeId: number, data: Omit<AdminUpdatePetInfoDto, 'qrCodeId'>): Promise<PetInfo> {
+    const petInfo = await this.petInfoRepository.findOne({
+      where: { qrCodeId }
+    })
+
+    if (!petInfo) {
+      throw new Error('宠物/物品信息不存在')
+    }
+
+    await this.petInfoRepository.update(petInfo.id, data)
+    const updated = await this.petInfoRepository.findOne({ where: { id: petInfo.id } })
+    if (!updated) {
+      throw new Error('更新后无法找到宠物/物品信息')
+    }
+    return updated
+  }
+
+  async query(
+    name?: string,
+    contactName?: string,
+    contactPhone?: string,
+    startDate?: string,
+    endDate?: string,
+    page: number = 0,
+    pageSize: number = 20
+  ): Promise<PaginationResponse<PetInfo>> {
+    const queryBuilder = this.petInfoRepository.createQueryBuilder('petInfo')
+
+    if (name) {
+      queryBuilder.andWhere('petInfo.name LIKE :name', { name: `%${name}%` })
+    }
+
+    if (contactName) {
+      queryBuilder.andWhere('petInfo.contactName LIKE :contactName', { contactName: `%${contactName}%` })
+    }
+
+    if (contactPhone) {
+      queryBuilder.andWhere('petInfo.contactPhone LIKE :contactPhone', { contactPhone: `%${contactPhone}%` })
+    }
+
+    if (startDate && endDate) {
+      queryBuilder.andWhere('DATE(petInfo.createdAt) BETWEEN :startDate AND :endDate', { startDate, endDate })
+    } else if (startDate) {
+      queryBuilder.andWhere('DATE(petInfo.createdAt) >= :startDate', { startDate })
+    } else if (endDate) {
+      queryBuilder.andWhere('DATE(petInfo.createdAt) <= :endDate', { endDate })
+    }
+
+    const [content, total] = await queryBuilder
+      .skip(page * pageSize)
+      .take(pageSize)
+      .orderBy('petInfo.createdAt', 'DESC')
+      .getManyAndCount()
+
+    return {
+      content,
+      metadata: {
+        total: Number(total),
+        page: Number(page),
+        size: Number(pageSize)
+      }
+    }
+  }
+}

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

@@ -0,0 +1,204 @@
+import { Repository, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'
+import { FastifyInstance } from 'fastify'
+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'
+
+export class QrCodeService {
+  private qrCodeRepository: Repository<QrCode>
+  private personInfoRepository: Repository<PersonInfo>
+  private petInfoRepository: Repository<PetInfo>
+
+  constructor(app: FastifyInstance) {
+    this.qrCodeRepository = app.dataSource.getRepository(QrCode)
+    this.personInfoRepository = app.dataSource.getRepository(PersonInfo)
+    this.petInfoRepository = app.dataSource.getRepository(PetInfo)
+  }
+
+  /**
+   * 生成唯一的二维码编号
+   */
+  private generateQrCode(): string {
+    const timestamp = Date.now().toString(36)
+    const random = randomBytes(8).toString('hex')
+    return `QR${timestamp}${random}`.toUpperCase()
+  }
+
+  /**
+   * 生成维护码
+   */
+  private generateMaintenanceCode(): string {
+    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+    let code = ''
+    for (let i = 0; i < 8; i++) {
+      code += chars.charAt(Math.floor(Math.random() * chars.length))
+    }
+    return code
+  }
+
+  /**
+   * 生成二维码
+   */
+  async generateQrCodes(
+    qrType: QrType,
+    quantity: number = 1
+  ): Promise<Array<{ qrCode: string; maintenanceCode: string }>> {
+    const result = []
+
+    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,
+        qrType,
+        isActivated: false,
+        scanCount: 0
+      })
+
+      await this.qrCodeRepository.save(entity)
+
+      result.push({
+        qrCode,
+        maintenanceCode
+      })
+    }
+
+    return result
+  }
+
+  /**
+   * 验证维护码
+   */
+  async verifyMaintenanceCode(qrCode: string, maintenanceCode: string): Promise<boolean> {
+    const entity = await this.qrCodeRepository.findOne({ where: { qrCode } })
+    if (!entity) {
+      return false
+    }
+    return bcrypt.compare(maintenanceCode, entity.maintenanceCode)
+  }
+
+  /**
+   * 根据二维码获取信息
+   */
+  async getQrCodeInfo(qrCode: string): Promise<any> {
+    const entity = await this.qrCodeRepository.findOne({ where: { qrCode } })
+    if (!entity) {
+      throw new Error('二维码不存在')
+    }
+
+    let info = null
+    if (entity.qrType === QrType.PERSON) {
+      info = await this.personInfoRepository.findOne({ where: { qrCodeId: entity.id } })
+    } else if (entity.qrType === QrType.PET) {
+      info = await this.petInfoRepository.findOne({ where: { qrCodeId: entity.id } })
+    }
+
+    return {
+      id: entity.id,
+      qrCode: entity.qrCode,
+      qrType: entity.qrType,
+      isActivated: entity.isActivated,
+      scanCount: entity.scanCount,
+      info
+    }
+  }
+
+  /**
+   * 增加扫描次数
+   */
+  async incrementScanCount(qrCodeId: number): Promise<void> {
+    await this.qrCodeRepository.increment({ id: qrCodeId }, 'scanCount', 1)
+  }
+
+  /**
+   * 激活二维码
+   */
+  async activateQrCode(qrCodeId: number): Promise<void> {
+    await this.qrCodeRepository.update(qrCodeId, { isActivated: true })
+  }
+
+  /**
+   * 查询二维码列表
+   */
+  async queryQrCodes(
+    qrType?: QrType,
+    isActivated?: boolean,
+    startDate?: string,
+    endDate?: string,
+    page: number = 0,
+    pageSize: number = 20
+  ): Promise<PaginationResponse<QrCode>> {
+    const queryBuilder = this.qrCodeRepository.createQueryBuilder('qrCode')
+
+    if (qrType) {
+      queryBuilder.andWhere('qrCode.qrType = :qrType', { qrType })
+    }
+
+    if (isActivated !== undefined) {
+      queryBuilder.andWhere('qrCode.isActivated = :isActivated', { isActivated })
+    }
+
+    if (startDate && endDate) {
+      queryBuilder.andWhere('DATE(qrCode.createdAt) BETWEEN :startDate AND :endDate', { startDate, endDate })
+    } else if (startDate) {
+      queryBuilder.andWhere('DATE(qrCode.createdAt) >= :startDate', { startDate })
+    } else if (endDate) {
+      queryBuilder.andWhere('DATE(qrCode.createdAt) <= :endDate', { endDate })
+    }
+
+    const [content, total] = await queryBuilder
+      .select([
+        'qrCode.id',
+        'qrCode.qrCode',
+        'qrCode.qrType',
+        'qrCode.isActivated',
+        'qrCode.scanCount',
+        'qrCode.createdAt',
+        'qrCode.updatedAt'
+      ])
+      .skip(page * pageSize)
+      .take(pageSize)
+      .orderBy('qrCode.createdAt', 'DESC')
+      .getManyAndCount()
+
+    return {
+      content,
+      metadata: {
+        total: Number(total),
+        page: Number(page),
+        size: Number(pageSize)
+      }
+    }
+  }
+
+  /**
+   * 根据日期获取二维码列表(用于下载)
+   */
+  async getQrCodesByDate(date: string): Promise<QrCode[]> {
+    return this.qrCodeRepository
+      .createQueryBuilder('qrCode')
+      .where('DATE(qrCode.createdAt) = :date', { date })
+      .orderBy('qrCode.createdAt', 'ASC')
+      .getMany()
+  }
+
+  /**
+   * 根据ID获取二维码
+   */
+  async findById(id: number): Promise<QrCode | null> {
+    return this.qrCodeRepository.findOne({ where: { id } })
+  }
+
+  /**
+   * 根据qrCode获取实体
+   */
+  async findByQrCode(qrCode: string): Promise<QrCode | null> {
+    return this.qrCodeRepository.findOne({ where: { qrCode } })
+  }
+}

+ 133 - 0
src/services/scan-record.service.ts

@@ -0,0 +1,133 @@
+import { Repository } from 'typeorm'
+import { FastifyInstance } from 'fastify'
+import { ScanRecord } from '../entities/scan-record.entity'
+import { QrCodeService } from './qr-code.service'
+import { PaginationResponse } from '../dto/common.dto'
+
+export class ScanRecordService {
+  private scanRecordRepository: Repository<ScanRecord>
+  private qrCodeService: QrCodeService
+
+  constructor(app: FastifyInstance) {
+    this.scanRecordRepository = app.dataSource.getRepository(ScanRecord)
+    this.qrCodeService = new QrCodeService(app)
+  }
+
+  /**
+   * 创建扫描记录
+   */
+  async create(
+    qrCode: string,
+    latitude?: number,
+    longitude?: number,
+    address?: string,
+    ipAddress?: string,
+    userAgent?: string
+  ): Promise<ScanRecord> {
+    // 获取二维码实体
+    const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+    if (!qrCodeEntity) {
+      throw new Error('二维码不存在')
+    }
+
+    // 创建扫描记录
+    const scanRecord = this.scanRecordRepository.create({
+      qrCodeId: qrCodeEntity.id,
+      scanTime: new Date(),
+      latitude,
+      longitude,
+      address,
+      ipAddress,
+      userAgent
+    })
+
+    const result = await this.scanRecordRepository.save(scanRecord)
+
+    // 增加扫描次数
+    await this.qrCodeService.incrementScanCount(qrCodeEntity.id)
+
+    return result
+  }
+
+  /**
+   * 获取最近N次扫描记录
+   */
+  async getRecentRecords(qrCode: string, limit: number = 10): Promise<ScanRecord[]> {
+    const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+    if (!qrCodeEntity) {
+      throw new Error('二维码不存在')
+    }
+
+    return this.scanRecordRepository.find({
+      where: { qrCodeId: qrCodeEntity.id },
+      order: { scanTime: 'DESC' },
+      take: limit
+    })
+  }
+
+  /**
+   * 获取某个二维码的所有扫描记录
+   */
+  async getAllRecords(qrCodeId: number): Promise<ScanRecord[]> {
+    return this.scanRecordRepository.find({
+      where: { qrCodeId },
+      order: { scanTime: 'DESC' }
+    })
+  }
+
+  /**
+   * 条件分页查询扫描记录
+   */
+  async queryRecords(
+    qrCode?: string,
+    startDate?: string,
+    endDate?: string,
+    page: number = 0,
+    pageSize: number = 20
+  ): Promise<PaginationResponse<ScanRecord>> {
+    const queryBuilder = this.scanRecordRepository.createQueryBuilder('scanRecord')
+
+    if (qrCode) {
+      const qrCodeEntity = await this.qrCodeService.findByQrCode(qrCode)
+      if (!qrCodeEntity) {
+        throw new Error('二维码不存在')
+      }
+      queryBuilder.andWhere('scanRecord.qrCodeId = :qrCodeId', { qrCodeId: qrCodeEntity.id })
+    }
+
+    if (startDate && endDate) {
+      queryBuilder.andWhere('scanRecord.scanTime BETWEEN :startDate AND :endDate', {
+        startDate: new Date(startDate),
+        endDate: new Date(endDate)
+      })
+    } else if (startDate) {
+      queryBuilder.andWhere('scanRecord.scanTime >= :startDate', { startDate: new Date(startDate) })
+    } else if (endDate) {
+      queryBuilder.andWhere('scanRecord.scanTime <= :endDate', { endDate: new Date(endDate) })
+    }
+
+    const [content, total] = await queryBuilder
+      .orderBy('scanRecord.scanTime', 'DESC')
+      .skip(page * pageSize)
+      .take(pageSize)
+      .getManyAndCount()
+
+    return {
+      content,
+      metadata: {
+        total: Number(total),
+        page: Number(page),
+        size: Number(pageSize)
+      }
+    }
+  }
+
+  /**
+   * 统计扫描次数
+   */
+  async countByQrCodeId(qrCodeId: number): Promise<number> {
+    return this.scanRecordRepository.count({
+      where: { qrCodeId }
+    })
+  }
+}