Files
plates-server/docs/push-service-architecture.md
2025-10-11 17:38:04 +08:00

18 KiB

iOS推送服务架构和接口设计

1. 服务架构概览

1.1 整体架构图

graph TB
    A[iOS App] --> B[APNs]
    B --> C[NestJS Push Service]
    C --> D[APNs Provider]
    D --> B
    C --> E[Push Token Service]
    C --> F[Push Template Service]
    C --> G[Push Message Service]
    C --> H[Database]
    E --> H
    F --> H
    G --> H

1.2 模块依赖关系

graph LR
    A[PushNotificationsModule] --> B[PushNotificationsController]
    A --> C[PushNotificationsService]
    A --> D[ApnsProvider]
    A --> E[PushTokenService]
    A --> F[PushTemplateService]
    A --> G[PushMessageService]
    A --> H[DatabaseModule]
    A --> I[ConfigModule]

2. 核心服务类设计

2.1 PushNotificationsService

@Injectable()
export class PushNotificationsService {
  constructor(
    private readonly apnsProvider: ApnsProvider,
    private readonly pushTokenService: PushTokenService,
    private readonly pushTemplateService: PushTemplateService,
    private readonly pushMessageService: PushMessageService,
    private readonly logger: Logger,
  ) {}

  // 发送单个推送
  async sendNotification(userId: string, notification: PushNotificationDto): Promise<PushResponseDto>
  
  // 批量发送推送
  async sendBatchNotifications(userIds: string[], notification: PushNotificationDto): Promise<BatchPushResponseDto>
  
  // 使用模板发送推送
  async sendNotificationByTemplate(userId: string, templateKey: string, data: any): Promise<PushResponseDto>
  
  // 发送静默推送
  async sendSilentNotification(userId: string, payload: any): Promise<PushResponseDto>
}

2.2 ApnsProvider

@Injectable()
export class ApnsProvider {
  private provider: apn.Provider;
  private multiProvider: apn.MultiProvider;

  constructor(private readonly configService: ConfigService) {
    this.initializeProvider();
  }

  // 初始化APNs连接
  private initializeProvider(): void
  
  // 发送单个通知
  async send(notification: apn.Notification, deviceTokens: string[]): Promise<apn.Results>
  
  // 批量发送通知
  async sendBatch(notifications: apn.Notification[], deviceTokens: string[]): Promise<apn.Results>
  
  // 管理推送通道
  async manageChannels(notification: apn.Notification, bundleId: string, action: string): Promise<any>
  
  // 广播实时活动通知
  async broadcast(notification: apn.Notification, bundleId: string): Promise<any>
  
  // 关闭连接
  shutdown(): void
}

2.3 PushTokenService

@Injectable()
export class PushTokenService {
  constructor(
    @InjectModel(UserPushToken) private readonly pushTokenModel: typeof UserPushToken,
  ) {}

  // 注册设备令牌
  async registerToken(userId: string, tokenData: RegisterDeviceTokenDto): Promise<UserPushToken>
  
  // 更新设备令牌
  async updateToken(userId: string, tokenData: UpdateDeviceTokenDto): Promise<UserPushToken>
  
  // 注销设备令牌
  async unregisterToken(userId: string, deviceToken: string): Promise<void>
  
  // 获取用户的所有有效令牌
  async getActiveTokens(userId: string): Promise<UserPushToken[]>
  
  // 清理无效令牌
  async cleanupInvalidTokens(): Promise<number>
  
  // 验证令牌有效性
  async validateToken(deviceToken: string): Promise<boolean>
}

2.4 PushTemplateService

@Injectable()
export class PushTemplateService {
  constructor(
    @InjectModel(PushTemplate) private readonly templateModel: typeof PushTemplate,
  ) {}

  // 创建推送模板
  async createTemplate(templateData: CreatePushTemplateDto): Promise<PushTemplate>
  
  // 更新推送模板
  async updateTemplate(id: string, templateData: UpdatePushTemplateDto): Promise<PushTemplate>
  
  // 删除推送模板
  async deleteTemplate(id: string): Promise<void>
  
  // 获取模板
  async getTemplate(templateKey: string): Promise<PushTemplate>
  
  // 获取所有模板
  async getAllTemplates(): Promise<PushTemplate[]>
  
  // 渲染模板
  async renderTemplate(templateKey: string, data: any): Promise<RenderedTemplate>
}

2.5 PushMessageService

@Injectable()
export class PushMessageService {
  constructor(
    @InjectModel(PushMessage) private readonly messageModel: typeof PushMessage,
  ) {}

  // 创建推送消息记录
  async createMessage(messageData: CreatePushMessageDto): Promise<PushMessage>
  
  // 更新消息状态
  async updateMessageStatus(id: string, status: PushMessageStatus, response?: any): Promise<void>
  
  // 获取消息历史
  async getMessageHistory(userId: string, options: QueryOptions): Promise<PushMessage[]>
  
  // 获取消息统计
  async getMessageStats(userId?: string, timeRange?: TimeRange): Promise<PushStats>
  
  // 清理过期消息
  async cleanupExpiredMessages(): Promise<number>
}

3. 数据传输对象(DTO)设计

3.1 推送令牌相关DTO

// 注册设备令牌
export class RegisterDeviceTokenDto {
  @ApiProperty({ description: '设备推送令牌' })
  @IsString()
  @IsNotEmpty()
  deviceToken: string;

  @ApiProperty({ description: '设备类型', enum: DeviceType })
  @IsEnum(DeviceType)
  deviceType: DeviceType;

  @ApiProperty({ description: '应用版本', required: false })
  @IsString()
  @IsOptional()
  appVersion?: string;

  @ApiProperty({ description: '操作系统版本', required: false })
  @IsString()
  @IsOptional()
  osVersion?: string;

  @ApiProperty({ description: '设备名称', required: false })
  @IsString()
  @IsOptional()
  deviceName?: string;
}

// 更新设备令牌
export class UpdateDeviceTokenDto {
  @ApiProperty({ description: '新的设备推送令牌' })
  @IsString()
  @IsNotEmpty()
  newDeviceToken: string;

  @ApiProperty({ description: '应用版本', required: false })
  @IsString()
  @IsOptional()
  appVersion?: string;

  @ApiProperty({ description: '操作系统版本', required: false })
  @IsString()
  @IsOptional()
  osVersion?: string;

  @ApiProperty({ description: '设备名称', required: false })
  @IsString()
  @IsOptional()
  deviceName?: string;
}

3.2 推送消息相关DTO

// 发送推送通知
export class SendPushNotificationDto {
  @ApiProperty({ description: '用户ID列表' })
  @IsArray()
  @IsString({ each: true })
  userIds: string[];

  @ApiProperty({ description: '推送标题' })
  @IsString()
  @IsNotEmpty()
  title: string;

  @ApiProperty({ description: '推送内容' })
  @IsString()
  @IsNotEmpty()
  body: string;

  @ApiProperty({ description: '自定义数据', required: false })
  @IsObject()
  @IsOptional()
  payload?: any;

  @ApiProperty({ description: '推送类型', enum: PushType, required: false })
  @IsEnum(PushType)
  @IsOptional()
  pushType?: PushType;

  @ApiProperty({ description: '优先级', required: false })
  @IsNumber()
  @IsOptional()
  priority?: number;

  @ApiProperty({ description: '过期时间(秒)', required: false })
  @IsNumber()
  @IsOptional()
  expiry?: number;

  @ApiProperty({ description: '折叠ID', required: false })
  @IsString()
  @IsOptional()
  collapseId?: string;
}

// 使用模板发送推送
export class SendPushByTemplateDto {
  @ApiProperty({ description: '用户ID列表' })
  @IsArray()
  @IsString({ each: true })
  userIds: string[];

  @ApiProperty({ description: '模板键' })
  @IsString()
  @IsNotEmpty()
  templateKey: string;

  @ApiProperty({ description: '模板数据' })
  @IsObject()
  @IsNotEmpty()
  data: any;

  @ApiProperty({ description: '自定义数据', required: false })
  @IsObject()
  @IsOptional()
  payload?: any;
}

3.3 推送模板相关DTO

// 创建推送模板
export class CreatePushTemplateDto {
  @ApiProperty({ description: '模板键' })
  @IsString()
  @IsNotEmpty()
  templateKey: string;

  @ApiProperty({ description: '模板标题' })
  @IsString()
  @IsNotEmpty()
  title: string;

  @ApiProperty({ description: '模板内容' })
  @IsString()
  @IsNotEmpty()
  body: string;

  @ApiProperty({ description: '负载模板', required: false })
  @IsObject()
  @IsOptional()
  payloadTemplate?: any;

  @ApiProperty({ description: '推送类型', enum: PushType, required: false })
  @IsEnum(PushType)
  @IsOptional()
  pushType?: PushType;

  @ApiProperty({ description: '优先级', required: false })
  @IsNumber()
  @IsOptional()
  priority?: number;
}

// 更新推送模板
export class UpdatePushTemplateDto {
  @ApiProperty({ description: '模板标题', required: false })
  @IsString()
  @IsOptional()
  title?: string;

  @ApiProperty({ description: '模板内容', required: false })
  @IsString()
  @IsOptional()
  body?: string;

  @ApiProperty({ description: '负载模板', required: false })
  @IsObject()
  @IsOptional()
  payloadTemplate?: any;

  @ApiProperty({ description: '推送类型', enum: PushType, required: false })
  @IsEnum(PushType)
  @IsOptional()
  pushType?: PushType;

  @ApiProperty({ description: '优先级', required: false })
  @IsNumber()
  @IsOptional()
  priority?: number;

  @ApiProperty({ description: '是否激活', required: false })
  @IsBoolean()
  @IsOptional()
  isActive?: boolean;
}

3.4 响应DTO

// 推送响应
export class PushResponseDto {
  @ApiProperty({ description: '响应代码' })
  code: ResponseCode;

  @ApiProperty({ description: '响应消息' })
  message: string;

  @ApiProperty({ description: '推送结果' })
  data: {
    success: boolean;
    sentCount: number;
    failedCount: number;
    results: PushResult[];
  };
}

// 批量推送响应
export class BatchPushResponseDto {
  @ApiProperty({ description: '响应代码' })
  code: ResponseCode;

  @ApiProperty({ description: '响应消息' })
  message: string;

  @ApiProperty({ description: '批量推送结果' })
  data: {
    totalUsers: number;
    totalTokens: number;
    successCount: number;
    failedCount: number;
    results: PushResult[];
  };
}

// 推送结果
export class PushResult {
  @ApiProperty({ description: '用户ID' })
  userId: string;

  @ApiProperty({ description: '设备令牌' })
  deviceToken: string;

  @ApiProperty({ description: '是否成功' })
  success: boolean;

  @ApiProperty({ description: '错误信息', required: false })
  @IsString()
  @IsOptional()
  error?: string;

  @ApiProperty({ description: 'APNs响应', required: false })
  @IsObject()
  @IsOptional()
  apnsResponse?: any;
}

4. 控制器接口设计

4.1 PushNotificationsController

@Controller('push-notifications')
@ApiTags('推送通知')
@UseGuards(JwtAuthGuard)
export class PushNotificationsController {
  constructor(private readonly pushNotificationsService: PushNotificationsService) {}

  // 注册设备令牌
  @Post('register-token')
  @ApiOperation({ summary: '注册设备推送令牌' })
  @ApiResponse({ status: 200, description: '注册成功', type: ResponseDto })
  async registerToken(
    @CurrentUser() user: AccessTokenPayload,
    @Body() registerTokenDto: RegisterDeviceTokenDto,
  ): Promise<ResponseDto> {
    return this.pushNotificationsService.registerToken(user.sub, registerTokenDto);
  }

  // 更新设备令牌
  @Put('update-token')
  @ApiOperation({ summary: '更新设备推送令牌' })
  @ApiResponse({ status: 200, description: '更新成功', type: ResponseDto })
  async updateToken(
    @CurrentUser() user: AccessTokenPayload,
    @Body() updateTokenDto: UpdateDeviceTokenDto,
  ): Promise<ResponseDto> {
    return this.pushNotificationsService.updateToken(user.sub, updateTokenDto);
  }

  // 注销设备令牌
  @Delete('unregister-token')
  @ApiOperation({ summary: '注销设备推送令牌' })
  @ApiResponse({ status: 200, description: '注销成功', type: ResponseDto })
  async unregisterToken(
    @CurrentUser() user: AccessTokenPayload,
    @Body() body: { deviceToken: string },
  ): Promise<ResponseDto> {
    return this.pushNotificationsService.unregisterToken(user.sub, body.deviceToken);
  }

  // 发送推送通知
  @Post('send')
  @ApiOperation({ summary: '发送推送通知' })
  @ApiResponse({ status: 200, description: '发送成功', type: PushResponseDto })
  async sendNotification(
    @Body() sendNotificationDto: SendPushNotificationDto,
  ): Promise<PushResponseDto> {
    return this.pushNotificationsService.sendNotification(sendNotificationDto);
  }

  // 使用模板发送推送
  @Post('send-by-template')
  @ApiOperation({ summary: '使用模板发送推送' })
  @ApiResponse({ status: 200, description: '发送成功', type: PushResponseDto })
  async sendNotificationByTemplate(
    @Body() sendByTemplateDto: SendPushByTemplateDto,
  ): Promise<PushResponseDto> {
    return this.pushNotificationsService.sendNotificationByTemplate(sendByTemplateDto);
  }

  // 批量发送推送
  @Post('send-batch')
  @ApiOperation({ summary: '批量发送推送' })
  @ApiResponse({ status: 200, description: '发送成功', type: BatchPushResponseDto })
  async sendBatchNotifications(
    @Body() sendBatchDto: SendPushNotificationDto,
  ): Promise<BatchPushResponseDto> {
    return this.pushNotificationsService.sendBatchNotifications(sendBatchDto);
  }
}

4.2 PushTemplateController

@Controller('push-notifications/templates')
@ApiTags('推送模板')
@UseGuards(JwtAuthGuard)
export class PushTemplateController {
  constructor(private readonly pushTemplateService: PushTemplateService) {}

  // 获取所有模板
  @Get()
  @ApiOperation({ summary: '获取所有推送模板' })
  @ApiResponse({ status: 200, description: '获取成功', type: [PushTemplate] })
  async getAllTemplates(): Promise<PushTemplate[]> {
    return this.pushTemplateService.getAllTemplates();
  }

  // 获取单个模板
  @Get(':templateKey')
  @ApiOperation({ summary: '获取推送模板' })
  @ApiResponse({ status: 200, description: '获取成功', type: PushTemplate })
  async getTemplate(
    @Param('templateKey') templateKey: string,
  ): Promise<PushTemplate> {
    return this.pushTemplateService.getTemplate(templateKey);
  }

  // 创建模板
  @Post()
  @ApiOperation({ summary: '创建推送模板' })
  @ApiResponse({ status: 201, description: '创建成功', type: PushTemplate })
  async createTemplate(
    @Body() createTemplateDto: CreatePushTemplateDto,
  ): Promise<PushTemplate> {
    return this.pushTemplateService.createTemplate(createTemplateDto);
  }

  // 更新模板
  @Put(':id')
  @ApiOperation({ summary: '更新推送模板' })
  @ApiResponse({ status: 200, description: '更新成功', type: PushTemplate })
  async updateTemplate(
    @Param('id') id: string,
    @Body() updateTemplateDto: UpdatePushTemplateDto,
  ): Promise<PushTemplate> {
    return this.pushTemplateService.updateTemplate(id, updateTemplateDto);
  }

  // 删除模板
  @Delete(':id')
  @ApiOperation({ summary: '删除推送模板' })
  @ApiResponse({ status: 200, description: '删除成功' })
  async deleteTemplate(@Param('id') id: string): Promise<void> {
    return this.pushTemplateService.deleteTemplate(id);
  }
}

5. 接口使用示例

5.1 注册设备令牌

POST /api/push-notifications/register-token
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "deviceToken": "a9d0ed10e9cfd022a61cb08753f49c5a0b0dfb383697bf9f9d750a1003da19c7",
  "deviceType": "IOS",
  "appVersion": "1.0.0",
  "osVersion": "iOS 15.0",
  "deviceName": "iPhone 13"
}

5.2 发送推送通知

POST /api/push-notifications/send
Content-Type: application/json

{
  "userIds": ["user_123", "user_456"],
  "title": "训练提醒",
  "body": "您今天的普拉提训练还未完成,快来打卡吧!",
  "payload": {
    "type": "training_reminder",
    "trainingId": "training_123"
  },
  "pushType": "ALERT",
  "priority": 10
}

5.3 使用模板发送推送

POST /api/push-notifications/send-by-template
Content-Type: application/json

{
  "userIds": ["user_123"],
  "templateKey": "training_reminder",
  "data": {
    "userName": "张三",
    "trainingName": "核心力量训练"
  },
  "payload": {
    "type": "training_reminder",
    "trainingId": "training_123"
  }
}

6. 错误处理

6.1 错误类型定义

export enum PushErrorCode {
  INVALID_DEVICE_TOKEN = 'INVALID_DEVICE_TOKEN',
  DEVICE_TOKEN_NOT_FOR_TOPIC = 'DEVICE_TOKEN_NOT_FOR_TOPIC',
  TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
  INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
  TEMPLATE_NOT_FOUND = 'TEMPLATE_NOT_FOUND',
  USER_NOT_FOUND = 'USER_NOT_FOUND',
  INVALID_PAYLOAD = 'INVALID_PAYLOAD',
}

export class PushException extends HttpException {
  constructor(
    errorCode: PushErrorCode,
    message: string,
    statusCode: HttpStatus = HttpStatus.BAD_REQUEST,
  ) {
    super({ code: errorCode, message }, statusCode);
  }
}

6.2 全局异常处理

@Catch(PushException, Error)
export class PushExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    
    if (exception instanceof PushException) {
      response.status(exception.getStatus()).json(exception.getResponse());
    } else {
      response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
        code: 'INTERNAL_SERVER_ERROR',
        message: '推送服务内部错误',
      });
    }
  }
}

7. 性能优化策略

7.1 连接池管理

  • 使用HTTP/2连接池提高并发性能
  • 实现连接复用和心跳保活
  • 动态调整连接池大小

7.2 批量处理优化

  • 实现批量推送减少网络请求
  • 使用队列系统处理大量推送请求
  • 实现推送优先级和限流机制

7.3 缓存策略

  • 缓存用户设备令牌减少数据库查询
  • 缓存推送模板提高渲染性能
  • 实现分布式缓存支持集群部署