打造高可用系统通知架构:站内信、短信、邮件、推送全方案解析

引言:被低估的系统通知

在现代软件系统中,通知功能如同空气般存在 ------ 用户习以为常,开发者却往往低估其复杂度。一个设计糟糕的通知系统可能导致用户流失(重要通知未送达)、资源浪费(无效通知泛滥)甚至法律风险(垃圾信息投诉)。

想象这样一个场景:用户在电商平台下单后,迟迟收不到订单确认通知,担心支付成功却未下单;而另一位用户在取消订阅后,仍持续收到营销短信,最终选择卸载应用。这两种情况,根源都在于通知系统设计的缺陷。

本文将系统讲解通知系统的设计理念、架构选型、实现方案和最佳实践,涵盖站内信、短信、邮件、电话、推送等全渠道通知,帮助你构建一个高可用、可扩展、用户友好的通知中心。

一、系统通知的核心价值与设计原则

1.1 通知的核心价值

系统通知并非简单的信息传递,它承载着多重核心价值:

  • 信息同步:确保用户及时获取系统状态变更(如订单状态更新、账户变动)
  • 用户唤醒:通过个性化通知召回沉默用户,提升活跃度
  • 安全验证:作为身份验证的重要环节(如验证码、登录提醒)
  • 服务闭环:完善用户操作流程,提升服务体验(如预约提醒、服务完成通知)

1.2 通知系统设计的六大原则

1.2.1 可靠性原则

通知系统必须保证 "该到的一定到",尤其是关键业务通知。实现可靠性需要考虑:

  • 消息持久化存储
  • 失败重试机制
  • 多级降级策略
  • 通知状态追踪
1.2.2 时效性原则

不同类型的通知有不同的时效要求:

  • 验证码:秒级送达
  • 订单通知:分钟级送达
  • 营销通知:可接受小时级延迟

设计时需根据通知类型设置优先级和超时策略。

1.2.3 可扩展性原则

业务发展会带来通知量的激增和渠道的扩展,系统需具备:

  • 水平扩展能力
  • 新渠道接入的低耦合设计
  • 流量削峰填谷的能力
1.2.4 合规性原则

各国对信息推送有严格法规(如中国的《个人信息保护法》),需实现:

  • 用户订阅 / 退订机制
  • 明确的发送频率限制
  • 完整的通知记录用于审计
1.2.5 个性化原则

支持用户对通知方式进行偏好设置:

  • 渠道选择(如仅邮件通知)
  • 时间选择(如工作时间不接收营销短信)
  • 内容粒度(如仅接收重要通知)
1.2.6 可观测性原则

完善的监控和日志系统:

  • 各渠道送达率统计
  • 失败原因分析
  • 实时告警机制

二、通知系统的核心架构设计

2.1 整体架构概览

一个完善的通知系统应采用分层架构,各层职责清晰,通过接口交互实现解耦。

各层核心职责:

  • 接入层:提供统一 API,负责请求验证、参数校验、限流
  • 核心层:处理通知逻辑,包括模板渲染、渠道选择、优先级排序
  • 渠道层:适配各种通知渠道,处理具体发送逻辑
  • 偏好中心:管理用户通知偏好设置
  • 存储层:持久化通知记录、用户设置等
  • 统计层:收集发送数据,提供分析和监控

2.2 核心业务流程

通知从产生到送达用户的完整流程如下:

三、数据模型设计

合理的数据模型是系统稳定运行的基础,以下是核心表结构设计。

3.1 通知表(t_notification)

存储所有通知的基本信息:

sql

复制代码
CREATE TABLE `t_notification` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `biz_type` varchar(64) NOT NULL COMMENT '业务类型(如:ORDER、LOGIN、MARKETING)',
  `biz_id` varchar(64) DEFAULT NULL COMMENT '业务ID,关联具体业务对象',
  `user_id` bigint NOT NULL COMMENT '接收用户ID',
  `title` varchar(255) NOT NULL COMMENT '通知标题',
  `content` text NOT NULL COMMENT '通知内容',
  `notification_type` tinyint NOT NULL COMMENT '通知类型:1-系统通知 2-营销通知 3-验证通知',
  `priority` tinyint NOT NULL DEFAULT 3 COMMENT '优先级:1-最高 2-高 3-中 4-低 5-最低',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-待发送 1-发送中 2-发送成功 3-发送失败 4-已取消',
  `sender` varchar(64) DEFAULT NULL COMMENT '发送者',
  `send_time` datetime DEFAULT NULL COMMENT '发送时间',
  `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
  `created_by` varchar(64) NOT NULL COMMENT '创建人',
  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_by` varchar(64) NOT NULL COMMENT '更新人',
  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_id_status` (`user_id`,`status`),
  KEY `idx_biz_type_biz_id` (`biz_type`,`biz_id`),
  KEY `idx_created_time` (`created_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知主表';

3.2 通知渠道表(t_notification_channel)

记录每个通知在各渠道的发送情况:

sql

复制代码
CREATE TABLE `t_notification_channel` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `notification_id` bigint NOT NULL COMMENT '关联通知ID',
  `channel_type` tinyint NOT NULL COMMENT '渠道类型:1-站内信 2-短信 3-邮件 4-极光推送 5-电话',
  `channel_code` varchar(64) DEFAULT NULL COMMENT '渠道编码(如短信服务商编码)',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-待发送 1-发送中 2-发送成功 3-发送失败',
  `send_content` text COMMENT '实际发送内容',
  `channel_response` text COMMENT '渠道返回信息',
  `channel_msg_id` varchar(128) DEFAULT NULL COMMENT '渠道消息ID',
  `retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数',
  `max_retry_count` int NOT NULL DEFAULT 3 COMMENT '最大重试次数',
  `next_retry_time` datetime DEFAULT NULL COMMENT '下次重试时间',
  `send_time` datetime DEFAULT NULL COMMENT '发送时间',
  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_notification_id` (`notification_id`),
  KEY `idx_channel_type_status` (`channel_type`,`status`),
  KEY `idx_next_retry_time` (`next_retry_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知渠道表';

3.3 用户通知偏好表(t_user_notification_preference)

存储用户对各类型通知的渠道偏好:

sql

复制代码
CREATE TABLE `t_user_notification_preference` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `biz_type` varchar(64) NOT NULL COMMENT '业务类型',
  `notification_type` tinyint NOT NULL COMMENT '通知类型:1-系统通知 2-营销通知 3-验证通知',
  `preferred_channels` varchar(32) NOT NULL COMMENT '偏好渠道,逗号分隔(1,2,3)',
  `is_enabled` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用:0-禁用 1-启用',
  `notify_time_range` varchar(32) DEFAULT '00:00-23:59' COMMENT '允许通知的时间范围',
  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_biz_type` (`user_id`,`biz_type`,`notification_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户通知偏好表';

3.4 通知模板表(t_notification_template)

存储各类通知的模板:

sql

复制代码
CREATE TABLE `t_notification_template` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `template_code` varchar(64) NOT NULL COMMENT '模板编码(唯一)',
  `biz_type` varchar(64) NOT NULL COMMENT '业务类型',
  `notification_type` tinyint NOT NULL COMMENT '通知类型:1-系统通知 2-营销通知 3-验证通知',
  `channel_type` tinyint NOT NULL COMMENT '渠道类型:1-站内信 2-短信 3-邮件 4-极光推送 5-电话',
  `title` varchar(255) NOT NULL COMMENT '模板标题',
  `content` text NOT NULL COMMENT '模板内容,支持变量替换',
  `params` varchar(1024) DEFAULT NULL COMMENT '参数列表,JSON格式',
  `priority` tinyint NOT NULL DEFAULT 3 COMMENT '默认优先级',
  `is_enabled` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用:0-禁用 1-启用',
  `created_by` varchar(64) NOT NULL COMMENT '创建人',
  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_by` varchar(64) NOT NULL COMMENT '更新人',
  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_template_code_channel` (`template_code`,`channel_type`),
  KEY `idx_biz_type_channel` (`biz_type`,`channel_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知模板表';

四、核心组件设计与实现

4.1 项目依赖配置

首先,我们需要配置项目的核心依赖,使用最新稳定版本:

xml

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>notification-center</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <lombok.version>1.18.30</lombok.version>
        <fastjson2.version>2.0.32</fastjson2.version>
        <guava.version>32.1.3-jre</guava.version>
        <springdoc.version>2.1.0</springdoc.version>
    </properties>

    <dependencies>
        <!-- Spring Boot 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <!-- 数据库 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- 工具类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        
        <!-- API文档 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        
        <!-- 短信、邮件等第三方SDK -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>dysmsapi20170525</artifactId>
            <version>2.0.24</version>
        </dependency>
        <dependency>
            <groupId>cn.jpush.api</groupId>
            <artifactId>jpush-client</artifactId>
            <version>3.8.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

4.2 核心枚举定义

首先定义系统中使用的核心枚举,统一管理常量:

java

复制代码
/**
 * 通知类型枚举
 *
 * @author ken
 */
public enum NotificationTypeEnum {
    SYSTEM(1, "系统通知"),
    MARKETING(2, "营销通知"),
    VERIFICATION(3, "验证通知");

    private final int code;
    private final String desc;

    NotificationTypeEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }

    /**
     * 根据编码获取枚举
     *
     * @param code 编码
     * @return 枚举
     */
    public static NotificationTypeEnum getByCode(int code) {
        for (NotificationTypeEnum type : values()) {
            if (type.code == code) {
                return type;
            }
        }
        return null;
    }
}

/**
 * 通知渠道类型枚举
 *
 * @author ken
 */
public enum ChannelTypeEnum {
    INBOX(1, "站内信"),
    SMS(2, "短信"),
    EMAIL(3, "邮件"),
    JPUSH(4, "极光推送"),
    PHONE(5, "电话");

    private final int code;
    private final String desc;

    ChannelTypeEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }

    /**
     * 根据编码获取枚举
     *
     * @param code 编码
     * @return 枚举
     */
    public static ChannelTypeEnum getByCode(int code) {
        for (ChannelTypeEnum type : values()) {
            if (type.code == code) {
                return type;
            }
        }
        return null;
    }
}

/**
 * 通知状态枚举
 *
 * @author ken
 */
public enum NotificationStatusEnum {
    PENDING(0, "待发送"),
    SENDING(1, "发送中"),
    SUCCESS(2, "发送成功"),
    FAIL(3, "发送失败"),
    CANCELED(4, "已取消");

    private final int code;
    private final String desc;

    NotificationStatusEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

/**
 * 优先级枚举
 *
 * @author ken
 */
public enum PriorityEnum {
    HIGHEST(1, "最高"),
    HIGH(2, "高"),
    MEDIUM(3, "中"),
    LOW(4, "低"),
    LOWEST(5, "最低");

    private final int code;
    private final String desc;

    PriorityEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

4.3 数据模型实体类

基于 MyBatis-Plus 实现实体类:

java

复制代码
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 通知主表实体
 *
 * @author ken
 */
@Data
@TableName("t_notification")
public class Notification {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 业务类型(如:ORDER、LOGIN、MARKETING)
     */
    private String bizType;

    /**
     * 业务ID,关联具体业务对象
     */
    private String bizId;

    /**
     * 接收用户ID
     */
    private Long userId;

    /**
     * 通知标题
     */
    private String title;

    /**
     * 通知内容
     */
    private String content;

    /**
     * 通知类型:1-系统通知 2-营销通知 3-验证通知
     */
    private Integer notificationType;

    /**
     * 优先级:1-最高 2-高 3-中 4-低 5-最低
     */
    private Integer priority;

    /**
     * 状态:0-待发送 1-发送中 2-发送成功 3-发送失败 4-已取消
     */
    private Integer status;

    /**
     * 发送者
     */
    private String sender;

    /**
     * 发送时间
     */
    private LocalDateTime sendTime;

    /**
     * 过期时间
     */
    private LocalDateTime expireTime;

    /**
     * 创建人
     */
    private String createdBy;

    /**
     * 创建时间
     */
    private LocalDateTime createdTime;

    /**
     * 更新人
     */
    private String updatedBy;

    /**
     * 更新时间
     */
    private LocalDateTime updatedTime;
}

/**
 * 通知渠道表实体
 *
 * @author ken
 */
@Data
@TableName("t_notification_channel")
public class NotificationChannel {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 关联通知ID
     */
    private Long notificationId;

    /**
     * 渠道类型:1-站内信 2-短信 3-邮件 4-极光推送 5-电话
     */
    private Integer channelType;

    /**
     * 渠道编码(如短信服务商编码)
     */
    private String channelCode;

    /**
     * 状态:0-待发送 1-发送中 2-发送成功 3-发送失败
     */
    private Integer status;

    /**
     * 实际发送内容
     */
    private String sendContent;

    /**
     * 渠道返回信息
     */
    private String channelResponse;

    /**
     * 渠道消息ID
     */
    private String channelMsgId;

    /**
     * 重试次数
     */
    private Integer retryCount;

    /**
     * 最大重试次数
     */
    private Integer maxRetryCount;

    /**
     * 下次重试时间
     */
    private LocalDateTime nextRetryTime;

    /**
     * 发送时间
     */
    private LocalDateTime sendTime;

    /**
     * 创建时间
     */
    private LocalDateTime createdTime;

    /**
     * 更新时间
     */
    private LocalDateTime updatedTime;
}

/**
 * 用户通知偏好表实体
 *
 * @author ken
 */
@Data
@TableName("t_user_notification_preference")
public class UserNotificationPreference {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 业务类型
     */
    private String bizType;

    /**
     * 通知类型:1-系统通知 2-营销通知 3-验证通知
     */
    private Integer notificationType;

    /**
     * 偏好渠道,逗号分隔(1,2,3)
     */
    private String preferredChannels;

    /**
     * 是否启用:0-禁用 1-启用
     */
    private Integer isEnabled;

    /**
     * 允许通知的时间范围
     */
    private String notifyTimeRange;

    /**
     * 创建时间
     */
    private LocalDateTime createdTime;

    /**
     * 更新时间
     */
    private LocalDateTime updatedTime;
}

/**
 * 通知模板表实体
 *
 * @author ken
 */
@Data
@TableName("t_notification_template")
public class NotificationTemplate {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 模板编码(唯一)
     */
    private String templateCode;

    /**
     * 业务类型
     */
    private String bizType;

    /**
     * 通知类型:1-系统通知 2-营销通知 3-验证通知
     */
    private Integer notificationType;

    /**
     * 渠道类型:1-站内信 2-短信 3-邮件 4-极光推送 5-电话
     */
    private Integer channelType;

    /**
     * 模板标题
     */
    private String title;

    /**
     * 模板内容,支持变量替换
     */
    private String content;

    /**
     * 参数列表,JSON格式
     */
    private String params;

    /**
     * 默认优先级
     */
    private Integer priority;

    /**
     * 是否启用:0-禁用 1-启用
     */
    private Integer isEnabled;

    /**
     * 创建人
     */
    private String createdBy;

    /**
     * 创建时间
     */
    private LocalDateTime createdTime;

    /**
     * 更新人
     */
    private String updatedBy;

    /**
     * 更新时间
     */
    private LocalDateTime updatedTime;
}

4.4 核心 DTO 与 VO

定义数据传输对象和视图对象:

java

复制代码
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.util.Map;

/**
 * 发送通知请求DTO
 *
 * @author ken
 */
@Data
@Schema(description = "发送通知请求参数")
public class SendNotificationDTO {
    /**
     * 业务类型
     */
    @Schema(description = "业务类型", example = "ORDER")
    private String bizType;

    /**
     * 业务ID
     */
    @Schema(description = "业务ID", example = "123456")
    private String bizId;

    /**
     * 接收用户ID
     */
    @Schema(description = "接收用户ID", example = "10001")
    private Long userId;

    /**
     * 通知类型:1-系统通知 2-营销通知 3-验证通知
     */
    @Schema(description = "通知类型", example = "1")
    private Integer notificationType;

    /**
     * 模板编码
     */
    @Schema(description = "模板编码", example = "ORDER_CREATE")
    private String templateCode;

    /**
     * 模板参数
     */
    @Schema(description = "模板参数")
    private Map<String, Object> templateParams;

    /**
     * 优先级:1-最高 2-高 3-中 4-低 5-最低
     */
    @Schema(description = "优先级", example = "2")
    private Integer priority;

    /**
     * 过期时间(时间戳,毫秒)
     */
    @Schema(description = "过期时间", example = "1717234567890")
    private Long expireTime;

    /**
     * 发送者
     */
    @Schema(description = "发送者", example = "system")
    private String sender;
}

/**
 * 通知发送结果VO
 *
 * @author ken
 */
@Data
@Schema(description = "通知发送结果")
public class NotificationResultVO {
    /**
     * 通知ID
     */
    @Schema(description = "通知ID")
    private Long notificationId;

    /**
     * 状态:0-待发送 1-发送中 2-发送成功 3-发送失败 4-已取消
     */
    @Schema(description = "状态")
    private Integer status;

    /**
     * 状态描述
     */
    @Schema(description = "状态描述")
    private String statusDesc;

    /**
     * 错误信息
     */
    @Schema(description = "错误信息")
    private String errorMsg;
}

4.5 Mapper 接口定义

基于 MyBatis-Plus 实现数据访问层:

java

复制代码
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.notification.entity.Notification;
import org.apache.ibatis.annotations.Mapper;

/**
 * 通知Mapper
 *
 * @author ken
 */
@Mapper
public interface NotificationMapper extends BaseMapper<Notification> {
}

/**
 * 通知渠道Mapper
 *
 * @author ken
 */
@Mapper
public interface NotificationChannelMapper extends BaseMapper<NotificationChannel> {
}

/**
 * 用户通知偏好Mapper
 *
 * @author ken
 */
@Mapper
public interface UserNotificationPreferenceMapper extends BaseMapper<UserNotificationPreference> {
}

/**
 * 通知模板Mapper
 *
 * @author ken
 */
@Mapper
public interface NotificationTemplateMapper extends BaseMapper<NotificationTemplate> {
}

4.6 服务层核心接口与实现

首先定义核心服务接口:

java

复制代码
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.notification.dto.SendNotificationDTO;
import com.example.notification.entity.Notification;
import com.example.notification.vo.NotificationResultVO;

/**
 * 通知服务接口
 *
 * @author ken
 */
public interface NotificationService extends IService<Notification> {
    /**
     * 发送通知
     *
     * @param dto 发送通知请求参数
     * @return 通知发送结果
     */
    NotificationResultVO sendNotification(SendNotificationDTO dto);

    /**
     * 处理通知发送(内部方法)
     *
     * @param notificationId 通知ID
     */
    void processNotification(Long notificationId);

    /**
     * 重试发送失败的通知
     *
     * @param limit 重试数量限制
     * @return 实际重试的通知数量
     */
    int retryFailedNotifications(int limit);
}

服务实现类:

java

复制代码
import cn.hutool.core.date.LocalDateTimeUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.notification.dto.SendNotificationDTO;
import com.example.notification.entity.Notification;
import com.example.notification.entity.NotificationChannel;
import com.example.notification.entity.NotificationTemplate;
import com.example.notification.entity.UserNotificationPreference;
import com.example.notification.enums.ChannelTypeEnum;
import com.example.notification.enums.NotificationStatusEnum;
import com.example.notification.enums.NotificationTypeEnum;
import com.example.notification.mapper.NotificationChannelMapper;
import com.example.notification.mapper.NotificationMapper;
import com.example.notification.mapper.NotificationTemplateMapper;
import com.example.notification.mapper.UserNotificationPreferenceMapper;
import com.example.notification.service.NotificationChannelService;
import com.example.notification.service.NotificationService;
import com.example.notification.service.NotificationTemplateService;
import com.example.notification.service.UserNotificationPreferenceService;
import com.example.notification.vo.NotificationResultVO;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 通知服务实现类
 *
 * @author ken
 */
@Slf4j
@Service
public class NotificationServiceImpl extends ServiceImpl<NotificationMapper, Notification> implements NotificationService {

    @Autowired
    private NotificationTemplateService notificationTemplateService;

    @Autowired
    private UserNotificationPreferenceService userNotificationPreferenceService;

    @Autowired
    private NotificationChannelService notificationChannelService;

    @Autowired
    private NotificationChannelMapper notificationChannelMapper;

    @Autowired
    private NotificationTemplateMapper notificationTemplateMapper;

    @Autowired
    private UserNotificationPreferenceMapper userPreferenceMapper;

    /**
     * 发送通知
     *
     * @param dto 发送通知请求参数
     * @return 通知发送结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public NotificationResultVO sendNotification(SendNotificationDTO dto) {
        log.info("开始处理通知发送请求: {}", JSON.toJSONString(dto));
        
        // 参数校验
        validateSendParams(dto);
        
        // 获取模板
        List<NotificationTemplate> templates = notificationTemplateService.getByTemplateCodeAndValid(
                dto.getTemplateCode());
        if (CollectionUtils.isEmpty(templates)) {
            log.error("模板不存在或已禁用: {}", dto.getTemplateCode());
            return buildResult(null, NotificationStatusEnum.FAIL.getCode(), 
                    NotificationStatusEnum.FAIL.getDesc(), "模板不存在或已禁用");
        }
        
        // 获取用户通知偏好
        UserNotificationPreference preference = userPreferenceMapper.selectOne(
                new LambdaQueryWrapper<UserNotificationPreference>()
                        .eq(UserNotificationPreference::getUserId, dto.getUserId())
                        .eq(UserNotificationPreference::getBizType, dto.getBizType())
                        .eq(UserNotificationPreference::getNotificationType, dto.getNotificationType())
                        .eq(UserNotificationPreference::getIsEnabled, 1)
        );
        
        // 如果用户禁用了该类型通知,直接返回成功但不发送
        if (ObjectUtils.isEmpty(preference) || preference.getIsEnabled() == 0) {
            log.info("用户已禁用该类型通知,不发送: userId={}, bizType={}, notificationType={}",
                    dto.getUserId(), dto.getBizType(), dto.getNotificationType());
            return buildResult(null, NotificationStatusEnum.SUCCESS.getCode(),
                    NotificationStatusEnum.SUCCESS.getDesc(), null);
        }
        
        // 过滤用户偏好的渠道
        List<Integer> preferredChannels = Lists.newArrayList();
        if (StringUtils.hasText(preference.getPreferredChannels())) {
            preferredChannels = Lists.newArrayList(preference.getPreferredChannels().split(","))
                    .stream()
                    .map(Integer::parseInt)
                    .collect(Collectors.toList());
        }
        
        // 筛选符合用户偏好的模板
        List<NotificationTemplate> matchedTemplates = templates.stream()
                .filter(template -> preferredChannels.contains(template.getChannelType()))
                .collect(Collectors.toList());
        
        if (CollectionUtils.isEmpty(matchedTemplates)) {
            log.info("没有符合用户偏好的通知渠道,不发送: userId={}, preferredChannels={}",
                    dto.getUserId(), preference.getPreferredChannels());
            return buildResult(null, NotificationStatusEnum.SUCCESS.getCode(),
                    NotificationStatusEnum.SUCCESS.getDesc(), null);
        }
        
        // 检查是否在允许的时间范围内
        if (!isInTimeRange(preference.getNotifyTimeRange())) {
            log.info("当前时间不在用户允许的通知时间范围内,不发送: userId={}, timeRange={}",
                    dto.getUserId(), preference.getNotifyTimeRange());
            return buildResult(null, NotificationStatusEnum.SUCCESS.getCode(),
                    NotificationStatusEnum.SUCCESS.getDesc(), null);
        }
        
        // 创建通知记录
        Notification notification = createNotification(dto);
        baseMapper.insert(notification);
        
        // 创建渠道通知记录
        createNotificationChannels(notification, matchedTemplates, dto.getTemplateParams());
        
        // 异步处理通知发送
        processNotificationAsync(notification.getId());
        
        return buildResult(notification.getId(), NotificationStatusEnum.PENDING.getCode(),
                NotificationStatusEnum.PENDING.getDesc(), null);
    }

    /**
     * 处理通知发送
     *
     * @param notificationId 通知ID
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void processNotification(Long notificationId) {
        log.info("开始处理通知发送: notificationId={}", notificationId);
        
        // 查询通知信息
        Notification notification = baseMapper.selectById(notificationId);
        if (ObjectUtils.isEmpty(notification)) {
            log.error("通知不存在: notificationId={}", notificationId);
            return;
        }
        
        // 如果通知已取消或已过期,不处理
        if (notification.getStatus() == NotificationStatusEnum.CANCELED.getCode() ||
                (ObjectUtils.isEmpty(notification.getExpireTime()) && 
                        notification.getExpireTime().isBefore(LocalDateTime.now()))) {
            log.info("通知已取消或已过期,不处理: notificationId={}, status={}, expireTime={}",
                    notificationId, notification.getStatus(), notification.getExpireTime());
            return;
        }
        
        // 更新通知状态为发送中
        notification.setStatus(NotificationStatusEnum.SENDING.getCode());
        notification.setUpdatedBy("system");
        notification.setUpdatedTime(LocalDateTime.now());
        baseMapper.updateById(notification);
        
        // 查询该通知的所有渠道记录
        List<NotificationChannel> channels = notificationChannelMapper.selectList(
                new LambdaQueryWrapper<NotificationChannel>()
                        .eq(NotificationChannel::getNotificationId, notificationId)
                        .in(NotificationChannel::getStatus, 
                                NotificationStatusEnum.PENDING.getCode(),
                                NotificationStatusEnum.FAIL.getCode())
        );
        
        if (CollectionUtils.isEmpty(channels)) {
            log.info("没有需要处理的通知渠道: notificationId={}", notificationId);
            updateNotificationStatus(notificationId, NotificationStatusEnum.SUCCESS.getCode());
            return;
        }
        
        // 逐个处理渠道发送
        boolean allSuccess = true;
        for (NotificationChannel channel : channels) {
            try {
                // 发送通知
                boolean sendSuccess = notificationChannelService.send(channel);
                
                if (sendSuccess) {
                    // 发送成功,更新状态
                    channel.setStatus(NotificationStatusEnum.SUCCESS.getCode());
                    channel.setSendTime(LocalDateTime.now());
                    channel.setUpdatedTime(LocalDateTime.now());
                    notificationChannelMapper.updateById(channel);
                } else {
                    allSuccess = false;
                    // 发送失败,判断是否需要重试
                    if (channel.getRetryCount() < channel.getMaxRetryCount()) {
                        // 计算下次重试时间(指数退避策略)
                        int retryCount = channel.getRetryCount() + 1;
                        LocalDateTime nextRetryTime = LocalDateTime.now().plusSeconds((long) Math.pow(2, retryCount));
                        
                        channel.setStatus(NotificationStatusEnum.FAIL.getCode());
                        channel.setRetryCount(retryCount);
                        channel.setNextRetryTime(nextRetryTime);
                        channel.setUpdatedTime(LocalDateTime.now());
                        notificationChannelMapper.updateById(channel);
                    } else {
                        // 达到最大重试次数,不再重试
                        channel.setStatus(NotificationStatusEnum.FAIL.getCode());
                        channel.setUpdatedTime(LocalDateTime.now());
                        notificationChannelMapper.updateById(channel);
                    }
                }
            } catch (Exception e) {
                allSuccess = false;
                log.error("处理通知渠道异常: channelId={}, notificationId={}", 
                        channel.getId(), notificationId, e);
                
                // 更新异常状态
                if (channel.getRetryCount() < channel.getMaxRetryCount()) {
                    int retryCount = channel.getRetryCount() + 1;
                    LocalDateTime nextRetryTime = LocalDateTime.now().plusSeconds((long) Math.pow(2, retryCount));
                    
                    channel.setStatus(NotificationStatusEnum.FAIL.getCode());
                    channel.setRetryCount(retryCount);
                    channel.setNextRetryTime(nextRetryTime);
                    channel.setUpdatedTime(LocalDateTime.now());
                    notificationChannelMapper.updateById(channel);
                } else {
                    channel.setStatus(NotificationStatusEnum.FAIL.getCode());
                    channel.setUpdatedTime(LocalDateTime.now());
                    notificationChannelMapper.updateById(channel);
                }
            }
        }
        
        // 更新通知主状态
        if (allSuccess) {
            updateNotificationStatus(notificationId, NotificationStatusEnum.SUCCESS.getCode());
        } else {
            // 检查是否还有可重试的渠道
            long retryableCount = channels.stream()
                    .filter(c -> c.getStatus() == NotificationStatusEnum.FAIL.getCode() &&
                            c.getRetryCount() < c.getMaxRetryCount())
                    .count();
            
            if (retryableCount > 0) {
                // 还有可重试的渠道,状态保持为发送中
                updateNotificationStatus(notificationId, NotificationStatusEnum.SENDING.getCode());
            } else {
                // 所有渠道都已达到最大重试次数,标记为失败
                updateNotificationStatus(notificationId, NotificationStatusEnum.FAIL.getCode());
            }
        }
        
        log.info("通知处理完成: notificationId={}, allSuccess={}", notificationId, allSuccess);
    }

    /**
     * 重试发送失败的通知
     *
     * @param limit 重试数量限制
     * @return 实际重试的通知数量
     */
    @Override
    public int retryFailedNotifications(int limit) {
        log.info("开始重试失败的通知,限制数量: {}", limit);
        
        // 查询需要重试的渠道通知
        List<NotificationChannel> channels = notificationChannelMapper.selectList(
                new LambdaQueryWrapper<NotificationChannel>()
                        .eq(NotificationChannel::getStatus, NotificationStatusEnum.FAIL.getCode())
                        .lt(NotificationChannel::getRetryCount, NotificationChannel::getMaxRetryCount)
                        .le(NotificationChannel::getNextRetryTime, LocalDateTime.now())
                        .last("LIMIT " + limit)
        );
        
        if (CollectionUtils.isEmpty(channels)) {
            log.info("没有需要重试的通知");
            return 0;
        }
        
        // 去重获取通知ID
        List<Long> notificationIds = channels.stream()
                .map(NotificationChannel::getNotificationId)
                .distinct()
                .collect(Collectors.toList());
        
        // 逐个重试通知
        for (Long notificationId : notificationIds) {
            try {
                processNotification(notificationId);
            } catch (Exception e) {
                log.error("重试通知失败: notificationId={}", notificationId, e);
            }
        }
        
        log.info("通知重试完成,共处理: {} 个通知", notificationIds.size());
        return notificationIds.size();
    }

    /**
     * 参数校验
     *
     * @param dto 发送通知请求参数
     */
    private void validateSendParams(SendNotificationDTO dto) {
        if (ObjectUtils.isEmpty(dto.getUserId())) {
            throw new IllegalArgumentException("用户ID不能为空");
        }
        if (!StringUtils.hasText(dto.getBizType())) {
            throw new IllegalArgumentException("业务类型不能为空");
        }
        if (ObjectUtils.isEmpty(dto.getNotificationType())) {
            throw new IllegalArgumentException("通知类型不能为空");
        }
        if (!StringUtils.hasText(dto.getTemplateCode())) {
            throw new IllegalArgumentException("模板编码不能为空");
        }
        if (ObjectUtils.isEmpty(dto.getPriority())) {
            dto.setPriority(3); // 默认中等优先级
        }
    }

    /**
     * 创建通知记录
     *
     * @param dto 发送通知请求参数
     * @return 通知实体
     */
    private Notification createNotification(SendNotificationDTO dto) {
        Notification notification = new Notification();
        notification.setBizType(dto.getBizType());
        notification.setBizId(dto.getBizId());
        notification.setUserId(dto.getUserId());
        notification.setNotificationType(dto.getNotificationType());
        notification.setPriority(dto.getPriority());
        notification.setStatus(NotificationStatusEnum.PENDING.getCode());
        notification.setSender(dto.getSender() != null ? dto.getSender() : "system");
        
        if (dto.getExpireTime() != null) {
            notification.setExpireTime(LocalDateTimeUtil.of(dto.getExpireTime()));
        }
        
        notification.setCreatedBy("system");
        notification.setCreatedTime(LocalDateTime.now());
        notification.setUpdatedBy("system");
        notification.setUpdatedTime(LocalDateTime.now());
        
        return notification;
    }

    /**
     * 创建渠道通知记录
     *
     * @param notification 通知实体
     * @param templates 通知模板列表
     * @param params 模板参数
     */
    private void createNotificationChannels(Notification notification, 
                                           List<NotificationTemplate> templates,
                                           Map<String, Object> params) {
        List<NotificationChannel> channels = Lists.newArrayList();
        
        for (NotificationTemplate template : templates) {
            // 渲染模板内容
            String content = renderTemplate(template.getContent(), params);
            String title = renderTemplate(template.getTitle(), params);
            
            // 更新主通知的标题和内容(取第一个模板的)
            if (channels.isEmpty()) {
                notification.setTitle(title);
                notification.setContent(content);
                baseMapper.updateById(notification);
            }
            
            NotificationChannel channel = new NotificationChannel();
            channel.setNotificationId(notification.getId());
            channel.setChannelType(template.getChannelType());
            channel.setStatus(NotificationStatusEnum.PENDING.getCode());
            channel.setSendContent(content);
            channel.setRetryCount(0);
            channel.setMaxRetryCount(3); // 默认最多重试3次
            channel.setNextRetryTime(LocalDateTime.now());
            channel.setCreatedTime(LocalDateTime.now());
            channel.setUpdatedTime(LocalDateTime.now());
            
            channels.add(channel);
        }
        
        // 批量插入渠道记录
        if (!CollectionUtils.isEmpty(channels)) {
            notificationChannelMapper.batchInsert(channels);
        }
    }

    /**
     * 渲染模板内容
     *
     * @param templateContent 模板内容
     * @param params 模板参数
     * @return 渲染后的内容
     */
    private String renderTemplate(String templateContent, Map<String, Object> params) {
        if (!StringUtils.hasText(templateContent) || CollectionUtils.isEmpty(params)) {
            return templateContent;
        }
        
        String content = templateContent;
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            content = content.replace("${" + key + "}", value != null ? value.toString() : "");
        }
        
        return content;
    }

    /**
     * 检查当前时间是否在允许的范围内
     *
     * @param timeRange 时间范围,格式如"08:00-22:00"
     * @return 是否在范围内
     */
    private boolean isInTimeRange(String timeRange) {
        // 默认允许所有时间
        if (!StringUtils.hasText(timeRange)) {
            return true;
        }
        
        try {
            String[] parts = timeRange.split("-");
            if (parts.length != 2) {
                log.warn("时间范围格式不正确: {}", timeRange);
                return true;
            }
            
            String startTimeStr = parts[0];
            String endTimeStr = parts[1];
            
            LocalDateTime now = LocalDateTime.now();
            int currentHour = now.getHour();
            int currentMinute = now.getMinute();
            int currentTotalMinutes = currentHour * 60 + currentMinute;
            
            String[] startParts = startTimeStr.split(":");
            int startHour = Integer.parseInt(startParts[0]);
            int startMinute = Integer.parseInt(startParts[1]);
            int startTotalMinutes = startHour * 60 + startMinute;
            
            String[] endParts = endTimeStr.split(":");
            int endHour = Integer.parseInt(endParts[0]);
            int endMinute = Integer.parseInt(endParts[1]);
            int endTotalMinutes = endHour * 60 + endMinute;
            
            // 处理跨天情况,如"23:00-01:00"
            if (startTotalMinutes <= endTotalMinutes) {
                return currentTotalMinutes >= startTotalMinutes && currentTotalMinutes <= endTotalMinutes;
            } else {
                return currentTotalMinutes >= startTotalMinutes || currentTotalMinutes <= endTotalMinutes;
            }
        } catch (Exception e) {
            log.error("解析时间范围异常: {}", timeRange, e);
            return true;
        }
    }

    /**
     * 异步处理通知发送
     *
     * @param notificationId 通知ID
     */
    private void processNotificationAsync(Long notificationId) {
        // 实际项目中可以使用线程池或消息队列异步处理
        new Thread(() -> {
            try {
                processNotification(notificationId);
            } catch (Exception e) {
                log.error("异步处理通知异常: notificationId={}", notificationId, e);
            }
        }).start();
    }

    /**
     * 更新通知状态
     *
     * @param notificationId 通知ID
     * @param status 状态
     */
    private void updateNotificationStatus(Long notificationId, int status) {
        Notification notification = new Notification();
        notification.setId(notificationId);
        notification.setStatus(status);
        notification.setSendTime(status == NotificationStatusEnum.SUCCESS.getCode() ? LocalDateTime.now() : null);
        notification.setUpdatedBy("system");
        notification.setUpdatedTime(LocalDateTime.now());
        baseMapper.updateById(notification);
    }

    /**
     * 构建返回结果
     *
     * @param notificationId 通知ID
     * @param status 状态
     * @param statusDesc 状态描述
     * @param errorMsg 错误信息
     * @return 通知结果VO
     */
    private NotificationResultVO buildResult(Long notificationId, int status, String statusDesc, String errorMsg) {
        NotificationResultVO result = new NotificationResultVO();
        result.setNotificationId(notificationId);
        result.setStatus(status);
        result.setStatusDesc(statusDesc);
        result.setErrorMsg(errorMsg);
        return result;
    }
}

4.7 渠道服务接口与实现

渠道服务是通知系统的关键,负责与各种外部服务对接:

java

复制代码
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.notification.entity.NotificationChannel;

/**
 * 通知渠道服务接口
 *
 * @author ken
 */
public interface NotificationChannelService extends IService<NotificationChannel> {
    /**
     * 发送渠道通知
     *
     * @param channel 渠道通知实体
     * @return 是否发送成功
     */
    boolean send(NotificationChannel channel);
}

渠道服务实现类:

java

复制代码
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.notification.entity.NotificationChannel;
import com.example.notification.enums.ChannelTypeEnum;
import com.example.notification.mapper.NotificationChannelMapper;
import com.example.notification.service.NotificationChannelService;
import com.example.notification.service.channel.EmailChannelService;
import com.example.notification.service.channel.InboxChannelService;
import com.example.notification.service.channel.JPushChannelService;
import com.example.notification.service.channel.SmsChannelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 通知渠道服务实现类
 *
 * @author ken
 */
@Slf4j
@Service
public class NotificationChannelServiceImpl extends ServiceImpl<NotificationChannelMapper, NotificationChannel> 
        implements NotificationChannelService {

    @Autowired
    private InboxChannelService inboxChannelService;

    @Autowired
    private SmsChannelService smsChannelService;

    @Autowired
    private EmailChannelService emailChannelService;

    @Autowired
    private JPushChannelService jPushChannelService;

    /**
     * 发送渠道通知
     *
     * @param channel 渠道通知实体
     * @return 是否发送成功
     */
    @Override
    public boolean send(NotificationChannel channel) {
        log.info("开始发送渠道通知: channelId={}, channelType={}", 
                channel.getId(), channel.getChannelType());
        
        ChannelTypeEnum channelType = ChannelTypeEnum.getByCode(channel.getChannelType());
        if (channelType == null) {
            log.error("未知的渠道类型: {}", channel.getChannelType());
            channel.setChannelResponse("未知的渠道类型");
            return false;
        }
        
        try {
            boolean success;
            String response;
            
            // 根据渠道类型调用不同的发送服务
            switch (channelType) {
                case INBOX:
                    response = inboxChannelService.send(channel);
                    success = true; // 站内信一般不会失败
                    break;
                case SMS:
                    response = smsChannelService.send(channel);
                    success = response.contains("success");
                    break;
                case EMAIL:
                    response = emailChannelService.send(channel);
                    success = response.contains("success");
                    break;
                case JPUSH:
                    response = jPushChannelService.send(channel);
                    success = response.contains("success");
                    break;
                case PHONE:
                    // 电话渠道实现类似,此处省略
                    response = "电话通知功能暂未实现";
                    success = false;
                    break;
                default:
                    response = "不支持的渠道类型: " + channelType.getDesc();
                    success = false;
            }
            
            // 记录渠道返回信息
            channel.setChannelResponse(response);
            log.info("渠道通知发送完成: channelId={}, success={}, response={}", 
                    channel.getId(), success, response);
            
            return success;
        } catch (Exception e) {
            log.error("渠道通知发送异常: channelId={}", channel.getId(), e);
            channel.setChannelResponse("发送异常: " + e.getMessage());
            return false;
        }
    }
}

4.8 具体渠道实现

下面以短信和邮件渠道为例,展示具体实现:

java

复制代码
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import com.example.notification.entity.NotificationChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * 短信渠道服务
 *
 * @author ken
 */
@Slf4j
@Service
public class SmsChannelService {

    @Value("${aliyun.sms.access-key-id}")
    private String accessKeyId;

    @Value("${aliyun.sms.access-key-secret}")
    private String accessKeySecret;

    @Value("${aliyun.sms.sign-name}")
    private String signName;

    @Value("${aliyun.sms.endpoint}")
    private String endpoint;

    /**
     * 发送短信
     *
     * @param channel 渠道通知实体
     * @return 发送结果
     */
    public String send(NotificationChannel channel) {
        try {
            // 获取接收手机号(实际项目中需要从用户信息中获取)
            String phoneNumber = getPhoneNumberByNotificationId(channel.getNotificationId());
            if (!StringUtils.hasText(phoneNumber)) {
                return "失败:用户手机号不存在";
            }
            
            // 创建阿里云客户端
            Config config = new Config()
                    .setAccessKeyId(accessKeyId)
                    .setAccessKeySecret(accessKeySecret);
            config.endpoint = endpoint;
            
            Client client = new Client(config);
            
            // 构建发送请求
            SendSmsRequest sendSmsRequest = new SendSmsRequest()
                    .setPhoneNumbers(phoneNumber)
                    .setSignName(signName)
                    .setTemplateCode(getSmsTemplateCode(channel))
                    .setTemplateParam(buildSmsTemplateParam(channel));
            
            // 发送短信
            SendSmsResponse response = client.sendSms(sendSmsRequest);
            
            // 处理响应结果
            if ("OK".equals(response.getBody().getCode())) {
                // 记录短信平台返回的消息ID
                channel.setChannelMsgId(response.getBody().getBizId());
                return "success: " + response.getBody().getMessage();
            } else {
                return "fail: " + response.getBody().getCode() + ", " + response.getBody().getMessage();
            }
        } catch (Exception e) {
            log.error("发送短信异常", e);
            return "error: " + e.getMessage();
        }
    }

    /**
     * 获取短信模板编码
     *
     * @param channel 渠道通知实体
     * @return 模板编码
     */
    private String getSmsTemplateCode(NotificationChannel channel) {
        // 实际项目中需要根据模板编码和渠道类型获取对应的短信模板CODE
        // 这里简化处理,实际应从数据库查询
        return "SMS_123456789";
    }

    /**
     * 构建短信模板参数
     *
     * @param channel 渠道通知实体
     * @return 模板参数JSON
     */
    private String buildSmsTemplateParam(NotificationChannel channel) {
        // 实际项目中需要根据短信模板要求构建参数
        // 这里简化处理
        return "{\"content\":\"" + channel.getSendContent() + "\"}";
    }

    /**
     * 根据通知ID获取接收手机号
     *
     * @param notificationId 通知ID
     * @return 手机号
     */
    private String getPhoneNumberByNotificationId(Long notificationId) {
        // 实际项目中需要根据通知ID查询用户信息获取手机号
        // 这里简化处理,返回测试手机号
        return "13800138000";
    }
}

/**
 * 邮件渠道服务
 *
 * @author ken
 */
@Slf4j
@Service
public class EmailChannelService {

    @Autowired
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String from;

    /**
     * 发送邮件
     *
     * @param channel 渠道通知实体
     * @return 发送结果
     */
    public String send(NotificationChannel channel) {
        try {
            // 获取接收邮箱(实际项目中需要从用户信息中获取)
            String email = getEmailByNotificationId(channel.getNotificationId());
            if (!StringUtils.hasText(email)) {
                return "失败:用户邮箱不存在";
            }
            
            // 构建邮件消息
            SimpleMailMessage message = new SimpleMailMessage();
            message.setFrom(from);
            message.setTo(email);
            
            // 从内容中提取标题(实际项目中标题应单独存储)
            String title = extractTitleFromContent(channel.getSendContent());
            message.setSubject(title);
            
            message.setText(channel.getSendContent());
            message.setSentDate(new Date());
            
            // 发送邮件
            mailSender.send(message);
            
            return "success: 邮件发送成功";
        } catch (Exception e) {
            log.error("发送邮件异常", e);
            return "error: " + e.getMessage();
        }
    }

    /**
     * 从内容中提取标题
     *
     * @param content 内容
     * @return 标题
     */
    private String extractTitleFromContent(String content) {
        // 实际项目中标题应单独存储,这里简化处理
        if (content.length() > 20) {
            return content.substring(0, 20) + "...";
        }
        return content;
    }

    /**
     * 根据通知ID获取接收邮箱
     *
     * @param notificationId 通知ID
     * @return 邮箱地址
     */
    private String getEmailByNotificationId(Long notificationId) {
        // 实际项目中需要根据通知ID查询用户信息获取邮箱
        // 这里简化处理,返回测试邮箱
        return "test@example.com";
    }
}

4.9 控制器实现

提供 API 接口供外部调用:

java

复制代码
import com.example.notification.dto.SendNotificationDTO;
import com.example.notification.service.NotificationService;
import com.example.notification.vo.NotificationResultVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 通知控制器
 *
 * @author ken
 */
@Slf4j
@RestController
@RequestMapping("/api/v1/notification")
@Tag(name = "通知管理", description = "通知发送及管理接口")
public class NotificationController {

    @Autowired
    private NotificationService notificationService;

    /**
     * 发送通知
     *
     * @param dto 发送通知请求参数
     * @return 通知发送结果
     */
    @PostMapping("/send")
    @Operation(summary = "发送通知", description = "发送系统通知到指定用户")
    public NotificationResultVO sendNotification(@RequestBody SendNotificationDTO dto) {
        log.info("收到发送通知请求: {}", dto);
        return notificationService.sendNotification(dto);
    }

    /**
     * 重试失败的通知
     *
     * @param limit 重试数量限制
     * @return 实际重试的通知数量
     */
    @PostMapping("/retry-failed")
    @Operation(summary = "重试失败的通知", description = "重试发送失败的通知")
    public int retryFailedNotifications(int limit) {
        log.info("收到重试失败通知请求,限制数量: {}", limit);
        return notificationService.retryFailedNotifications(limit);
    }
}

五、高级特性设计

5.1 消息队列集成

为提高系统的可靠性和吞吐量,引入消息队列处理通知发送:

java

复制代码
import com.alibaba.fastjson2.JSON;
import com.example.notification.service.NotificationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 通知消息消费者
 *
 * @author ken
 */
@Slf4j
@Component
public class NotificationMessageConsumer {

    @Autowired
    private NotificationService notificationService;

    /**
     * 处理通知发送消息
     *
     * @param message 消息内容(通知ID)
     */
    @RabbitListener(queues = "notification.send.queue")
    public void handleNotificationSend(String message) {
        try {
            log.info("收到通知发送消息: {}", message);
            Long notificationId = Long.parseLong(message);
            notificationService.processNotification(notificationId);
        } catch (Exception e) {
            log.error("处理通知发送消息异常: {}", message, e);
            // 异常处理,可根据需要决定是否重新入队
        }
    }

    /**
     * 处理通知重试消息
     *
     * @param message 消息内容(通知ID)
     */
    @RabbitListener(queues = "notification.retry.queue")
    public void handleNotificationRetry(String message) {
        try {
            log.info("收到通知重试消息: {}", message);
            Long notificationId = Long.parseLong(message);
            notificationService.processNotification(notificationId);
        } catch (Exception e) {
            log.error("处理通知重试消息异常: {}", message, e);
        }
    }
}

/**
 * 通知消息生产者
 *
 * @author ken
 */
@Slf4j
@Component
public class NotificationMessageProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送通知消息到队列
     *
     * @param notificationId 通知ID
     */
    public void sendNotificationMessage(Long notificationId) {
        try {
            log.info("发送通知消息到队列: notificationId={}", notificationId);
            rabbitTemplate.convertAndSend("notification.send.exchange", "notification.send.key", 
                    notificationId.toString());
        } catch (Exception e) {
            log.error("发送通知消息异常: notificationId={}", notificationId, e);
            // 消息发送失败处理,可考虑本地持久化后重试
        }
    }

    /**
     * 发送通知重试消息到队列
     *
     * @param notificationId 通知ID
     * @param delay 延迟时间(毫秒)
     */
    public void sendNotificationRetryMessage(Long notificationId, long delay) {
        try {
            log.info("发送通知重试消息到队列: notificationId={}, delay={}", notificationId, delay);
            rabbitTemplate.convertAndSend("notification.retry.exchange", "notification.retry.key",
                    notificationId.toString(), message -> {
                        message.getMessageProperties().setDelay((int) delay);
                        return message;
                    });
        } catch (Exception e) {
            log.error("发送通知重试消息异常: notificationId={}", notificationId, e);
        }
    }
}

5.2 分布式锁实现

为避免重复发送和并发问题,实现分布式锁:

java

复制代码
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 分布式锁工具类
 *
 * @author ken
 */
@Component
public class DistributedLockUtil {

    private final RedisTemplate<String, String> redisTemplate;

    // 锁的前缀
    private static final String LOCK_PREFIX = "notification:lock:";
    
    // 锁的默认过期时间(秒)
    private static final long DEFAULT_EXPIRE = 30;

    public DistributedLockUtil(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 获取锁
     *
     * @param key 锁的键
     * @return 是否获取成功
     */
    public boolean lock(String key) {
        return lock(key, DEFAULT_EXPIRE, TimeUnit.SECONDS);
    }

    /**
     * 获取锁
     *
     * @param key 锁的键
     * @param expire 过期时间
     * @param timeUnit 时间单位
     * @return 是否获取成功
     */
    public boolean lock(String key, long expire, TimeUnit timeUnit) {
        String lockKey = LOCK_PREFIX + key;
        // 使用UUID作为value,用于释放锁时的验证
        String value = UUID.randomUUID().toString();
        
        // 使用setIfAbsent实现分布式锁
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, value, expire, timeUnit);
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁
     *
     * @param key 锁的键
     */
    public void unlock(String key) {
        String lockKey = LOCK_PREFIX + key;
        String value = redisTemplate.opsForValue().get(lockKey);
        
        if (StringUtils.hasText(value)) {
            // 确保释放的是自己加的锁
            redisTemplate.delete(lockKey);
        }
    }
}

5.3 监控与统计

实现通知系统的监控和统计功能:

java

复制代码
import com.example.notification.entity.Notification;
import com.example.notification.entity.NotificationChannel;
import com.example.notification.enums.ChannelTypeEnum;
import com.example.notification.enums.NotificationStatusEnum;
import com.example.notification.mapper.NotificationChannelMapper;
import com.example.notification.mapper.NotificationMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 通知统计服务
 *
 * @author ken
 */
@Component
@Slf4j
public class NotificationStatisticService {

    @Autowired
    private NotificationMapper notificationMapper;

    @Autowired
    private NotificationChannelMapper channelMapper;

    @Autowired
    private NotificationStatisticMapper statisticMapper;

    /**
     * 每日统计通知发送情况
     * 每天凌晨2点执行
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void dailyStatistic() {
        log.info("开始执行每日通知统计");
        
        LocalDate yesterday = LocalDate.now().minusDays(1);
        LocalDateTime start = yesterday.atStartOfDay();
        LocalDateTime end = yesterday.atTime(23, 59, 59);
        
        // 统计通知总数和各状态数量
        statisticNotificationByDay(start, end, yesterday);
        
        // 统计各渠道发送情况
        statisticChannelByDay(start, end, yesterday);
        
        log.info("每日通知统计执行完成");
    }

    /**
     * 统计每日通知总体情况
     *
     * @param start 开始时间
     * @param end 结束时间
     * @param statDate 统计日期
     */
    private void statisticNotificationByDay(LocalDateTime start, LocalDateTime end, LocalDate statDate) {
        // 查询当天所有通知
        List<Notification> notifications = notificationMapper.selectList(
                new LambdaQueryWrapper<Notification>()
                        .between(Notification::getCreatedTime, start, end)
        );
        
        if (CollectionUtils.isEmpty(notifications)) {
            log.info("{} 没有通知记录", statDate);
            return;
        }
        
        // 总数量
        long total = notifications.size();
        
        // 按状态分组统计
        Map<Integer, Long> statusCount = notifications.stream()
                .collect(Collectors.groupingBy(Notification::getStatus, Collectors.counting()));
        
        // 按类型分组统计
        Map<Integer, Long> typeCount = notifications.stream()
                .collect(Collectors.groupingBy(Notification::getNotificationType, Collectors.counting()));
        
        // 构建统计数据并保存
        NotificationDailyStatistic statistic = new NotificationDailyStatistic();
        statistic.setStatDate(statDate);
        statistic.setTotalCount(total);
        statistic.setSuccessCount(statusCount.getOrDefault(NotificationStatusEnum.SUCCESS.getCode(), 0L));
        statistic.setFailCount(statusCount.getOrDefault(NotificationStatusEnum.FAIL.getCode(), 0L));
        statistic.setSystemCount(typeCount.getOrDefault(NotificationTypeEnum.SYSTEM.getCode(), 0L));
        statistic.setMarketingCount(typeCount.getOrDefault(NotificationTypeEnum.MARKETING.getCode(), 0L));
        statistic.setVerificationCount(typeCount.getOrDefault(NotificationTypeEnum.VERIFICATION.getCode(), 0L));
        statistic.setCreateTime(LocalDateTime.now());
        
        statisticMapper.insert(statistic);
    }

    /**
     * 统计每日各渠道发送情况
     *
     * @param start 开始时间
     * @param end 结束时间
     * @param statDate 统计日期
     */
    private void statisticChannelByDay(LocalDateTime start, LocalDateTime end, LocalDate statDate) {
        // 查询当天所有渠道通知
        List<NotificationChannel> channels = channelMapper.selectList(
                new LambdaQueryWrapper<NotificationChannel>()
                        .between(NotificationChannel::getCreatedTime, start, end)
        );
        
        if (CollectionUtils.isEmpty(channels)) {
            log.info("{} 没有渠道通知记录", statDate);
            return;
        }
        
        // 按渠道类型和状态分组统计
        Map<Integer, Map<Integer, Long>> channelStatusCount = channels.stream()
                .collect(Collectors.groupingBy(NotificationChannel::getChannelType,
                        Collectors.groupingBy(NotificationChannel::getStatus, Collectors.counting())));
        
        // 保存各渠道统计数据
        for (Map.Entry<Integer, Map<Integer, Long>> channelEntry : channelStatusCount.entrySet()) {
            Integer channelType = channelEntry.getKey();
            Map<Integer, Long> statusMap = channelEntry.getValue();
            
            NotificationChannelDailyStatistic statistic = new NotificationChannelDailyStatistic();
            statistic.setStatDate(statDate);
            statistic.setChannelType(channelType);
            statistic.setChannelName(ChannelTypeEnum.getByCode(channelType) != null ? 
                    ChannelTypeEnum.getByCode(channelType).getDesc() : "未知");
            statistic.setTotalCount(statusMap.values().stream().mapToLong(Long::longValue).sum());
            statistic.setSuccessCount(statusMap.getOrDefault(NotificationStatusEnum.SUCCESS.getCode(), 0L));
            statistic.setFailCount(statusMap.getOrDefault(NotificationStatusEnum.FAIL.getCode(), 0L));
            statistic.setCreateTime(LocalDateTime.now());
            
            statisticMapper.insertChannelStatistic(statistic);
        }
    }
}

六、最佳实践与性能优化

6.1 批量发送优化

对于营销类通知,经常需要批量发送,可采用以下优化策略:

  1. 异步批量处理:将批量通知放入消息队列,异步处理
  2. 分批次发送:将大量用户分成多个批次,避免瞬间流量过大
  3. 模板预加载:提前加载模板,避免重复查询数据库
  4. 连接池优化:针对短信、邮件等外部服务,优化连接池配置

java

复制代码
/**
 * 批量通知服务
 *
 * @author ken
 */
@Service
@Slf4j
public class BatchNotificationService {

    @Autowired
    private NotificationTemplateService templateService;

    @Autowired
    private NotificationMessageProducer messageProducer;

    @Autowired
    private NotificationMapper notificationMapper;

    // 每批处理的用户数量
    private static final int BATCH_SIZE = 1000;

    /**
     * 批量发送通知
     *
     * @param batchDTO 批量发送参数
     * @return 批量任务ID
     */
    public String batchSend(BatchNotificationDTO batchDTO) {
        log.info("开始处理批量通知: {}", JSON.toJSONString(batchDTO));
        
        // 参数校验
        validateBatchParams(batchDTO);
        
        // 生成批次ID
        String batchId = UUID.randomUUID().toString();
        
        // 获取接收用户ID列表
        List<Long> userIds = batchDTO.getUserIds();
        if (CollectionUtils.isEmpty(userIds)) {
            userIds = getUserIdsByCondition(batchDTO.getUserCondition());
        }
        
        if (CollectionUtils.isEmpty(userIds)) {
            log.warn("没有符合条件的用户,不发送批量通知");
            return batchId;
        }
        
        // 预加载模板
        List<NotificationTemplate> templates = templateService.getByTemplateCodeAndValid(
                batchDTO.getTemplateCode());
        if (CollectionUtils.isEmpty(templates)) {
            throw new IllegalArgumentException("模板不存在或已禁用: " + batchDTO.getTemplateCode());
        }
        
        // 分批次处理
        List<List<Long>> batches = Lists.partition(userIds, BATCH_SIZE);
        int totalBatch = batches.size();
        
        log.info("批量通知分批次处理: totalUser={}, batchSize={}, totalBatch={}",
                userIds.size(), BATCH_SIZE, totalBatch);
        
        // 记录批次信息
        saveBatchRecord(batchDTO, batchId, userIds.size(), totalBatch);
        
        // 异步处理各批次
        for (int i = 0; i < totalBatch; i++) {
            int batchNum = i + 1;
            List<Long> batchUserIds = batches.get(i);
            
            // 发送批次消息到队列
            sendBatchMessage(batchDTO, batchId, batchNum, totalBatch, batchUserIds);
        }
        
        return batchId;
    }

    /**
     * 发送批次消息到队列
     */
    private void sendBatchMessage(BatchNotificationDTO batchDTO, String batchId, 
                                 int batchNum, int totalBatch, List<Long> userIds) {
        try {
            BatchNotificationMessage message = new BatchNotificationMessage();
            message.setBatchId(batchId);
            message.setBatchNum(batchNum);
            message.setTotalBatch(totalBatch);
            message.setUserIds(userIds);
            message.setBizType(batchDTO.getBizType());
            message.setBizId(batchDTO.getBizId());
            message.setNotificationType(batchDTO.getNotificationType());
            message.setTemplateCode(batchDTO.getTemplateCode());
            message.setTemplateParams(batchDTO.getTemplateParams());
            message.setPriority(batchDTO.getPriority());
            message.setSender(batchDTO.getSender());
            
            // 发送到消息队列
            rabbitTemplate.convertAndSend("notification.batch.exchange", "notification.batch.key",
                    JSON.toJSONString(message));
            
            log.info("批次消息发送成功: batchId={}, batchNum={}/{}", batchId, batchNum, totalBatch);
        } catch (Exception e) {
            log.error("发送批次消息异常: batchId={}, batchNum={}/{}", 
                    batchId, batchNum, totalBatch, e);
            // 处理发送失败,可记录到本地重试表
        }
    }
}

6.2 缓存策略

对频繁访问的数据进行缓存,提高系统性能:

  1. 模板缓存:缓存通知模板,减少数据库查询
  2. 用户偏好缓存:缓存用户的通知偏好设置
  3. 发送频率缓存:缓存用户的发送记录,控制发送频率

java

复制代码
/**
 * 通知缓存服务
 *
 * @author ken
 */
@Service
public class NotificationCacheService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 模板缓存前缀
    private static final String TEMPLATE_CACHE_PREFIX = "notification:template:";
    
    // 用户偏好缓存前缀
    private static final String PREFERENCE_CACHE_PREFIX = "notification:preference:";
    
    // 发送频率限制缓存前缀
    private static final String RATE_LIMIT_CACHE_PREFIX = "notification:ratelimit:";
    
    // 模板缓存过期时间(小时)
    private static final long TEMPLATE_CACHE_EXPIRE = 24;
    
    // 用户偏好缓存过期时间(小时)
    private static final long PREFERENCE_CACHE_EXPIRE = 12;

    /**
     * 获取缓存的模板
     */
    public NotificationTemplate getTemplateFromCache(String templateCode, Integer channelType) {
        String key = TEMPLATE_CACHE_PREFIX + templateCode + ":" + channelType;
        return (NotificationTemplate) redisTemplate.opsForValue().get(key);
    }

    /**
     * 缓存模板
     */
    public void cacheTemplate(NotificationTemplate template) {
        String key = TEMPLATE_CACHE_PREFIX + template.getTemplateCode() + ":" + template.getChannelType();
        redisTemplate.opsForValue().set(key, template, TEMPLATE_CACHE_EXPIRE, TimeUnit.HOURS);
    }

    /**
     * 清除模板缓存
     */
    public void clearTemplateCache(String templateCode) {
        Set<String> keys = redisTemplate.keys(TEMPLATE_CACHE_PREFIX + templateCode + ":*");
        if (!CollectionUtils.isEmpty(keys)) {
            redisTemplate.delete(keys);
        }
    }

    /**
     * 获取缓存的用户偏好
     */
    public UserNotificationPreference getPreferenceFromCache(Long userId, String bizType, Integer notificationType) {
        String key = PREFERENCE_CACHE_PREFIX + userId + ":" + bizType + ":" + notificationType;
        return (UserNotificationPreference) redisTemplate.opsForValue().get(key);
    }

    /**
     * 缓存用户偏好
     */
    public void cachePreference(UserNotificationPreference preference) {
        String key = PREFERENCE_CACHE_PREFIX + preference.getUserId() + ":" + 
                preference.getBizType() + ":" + preference.getNotificationType();
        redisTemplate.opsForValue().set(key, preference, PREFERENCE_CACHE_EXPIRE, TimeUnit.HOURS);
    }

    /**
     * 检查并更新发送频率限制
     *
     * @param userId 用户ID
     * @param bizType 业务类型
     * @param maxCount 最大次数
     * @param period 时间周期(秒)
     * @return 是否允许发送
     */
    public boolean checkAndUpdateRateLimit(Long userId, String bizType, int maxCount, int period) {
        String key = RATE_LIMIT_CACHE_PREFIX + userId + ":" + bizType;
        
        // 使用Redis的incr命令实现计数器
        Long count = redisTemplate.opsForValue().increment(key);
        
        // 第一次设置过期时间
        if (count != null && count == 1) {
            redisTemplate.expire(key, period, TimeUnit.SECONDS);
        }
        
        // 检查是否超过限制
        return count != null && count <= maxCount;
    }
}

6.3 降级策略

当系统压力过大或外部服务异常时,实施降级策略:

  1. 渠道降级:当某一渠道不可用时,自动切换到备用渠道
  2. 优先级降级:高压力时,优先发送高优先级通知
  3. 内容降级:简化通知内容,减少处理时间

java

运行

复制代码
/**
 * 通知降级服务
 *
 * @author ken
 */
@Service
@Slf4j
public class NotificationDegradeService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 降级开关缓存键
    private static final String DEGRADE_SWITCH_KEY = "notification:degrade:switch";
    
    // 渠道健康状态缓存键前缀
    private static final String CHANNEL_HEALTH_KEY_PREFIX = "notification:channel:health:";
    
    // 系统负载缓存键
    private static final String SYSTEM_LOAD_KEY = "notification:system:load";

    /**
     * 检查是否需要全局降级
     *
     * @return 是否需要降级
     */
    public boolean needGlobalDegrade() {
        // 检查全局降级开关
        Object switchValue = redisTemplate.opsForValue().get(DEGRADE_SWITCH_KEY);
        if (switchValue != null && "true".equals(switchValue.toString())) {
            log.warn("全局降级开关已开启");
            return true;
        }
        
        // 检查系统负载
        Object loadValue = redisTemplate.opsForValue().get(SYSTEM_LOAD_KEY);
        if (loadValue != null) {
            try {
                double load = Double.parseDouble(loadValue.toString());
                // 系统负载超过阈值(如CPU使用率80%)
                if (load > 0.8) {
                    log.warn("系统负载过高,触发降级: load={}", load);
                    return true;
                }
            } catch (Exception e) {
                log.error("解析系统负载异常", e);
            }
        }
        
        return false;
    }

    /**
     * 检查渠道是否健康
     *
     * @param channelType 渠道类型
     * @return 是否健康
     */
    public boolean isChannelHealthy(int channelType) {
        String key = CHANNEL_HEALTH_KEY_PREFIX + channelType;
        Object healthValue = redisTemplate.opsForValue().get(key);
        
        // 默认健康
        if (healthValue == null) {
            return true;
        }
        
        try {
            // 健康度低于阈值(如50%)则认为不健康
            double health = Double.parseDouble(healthValue.toString());
            return health > 0.5;
        } catch (Exception e) {
            log.error("解析渠道健康度异常: channelType={}", channelType, e);
            return false;
        }
    }

    /**
     * 获取降级后的渠道列表
     *
     * @param originalChannels 原始渠道列表
     * @param notificationType 通知类型
     * @return 降级后的渠道列表
     */
    public List<Integer> getDegradedChannels(List<Integer> originalChannels, int notificationType) {
        // 过滤不健康的渠道
        List<Integer> healthyChannels = originalChannels.stream()
                .filter(this::isChannelHealthy)
                .collect(Collectors.toList());
        
        // 如果是验证类通知,必须保证至少有一个渠道可用
        if (notificationType == NotificationTypeEnum.VERIFICATION.getCode() && 
                CollectionUtils.isEmpty(healthyChannels)) {
            log.warn("验证类通知所有渠道都不健康,强制使用原始渠道");
            return originalChannels;
        }
        
        // 如果需要全局降级,并且是营销类通知,只保留站内信渠道
        if (needGlobalDegrade() && 
                notificationType == NotificationTypeEnum.MARKETING.getCode()) {
            log.warn("全局降级,营销类通知只保留站内信渠道");
            return healthyChannels.stream()
                    .filter(channel -> channel == ChannelTypeEnum.INBOX.getCode())
                    .collect(Collectors.toList());
        }
        
        return healthyChannels;
    }

    /**
     * 降级通知内容
     *
     * @param content 原始内容
     * @param notificationType 通知类型
     * @return 降级后的内容
     */
    public String degradeContent(String content, int notificationType) {
        // 全局降级且是营销类通知,简化内容
        if (needGlobalDegrade() && 
                notificationType == NotificationTypeEnum.MARKETING.getCode()) {
            // 内容太长则截断
            if (content.length() > 20) {
                return content.substring(0, 20) + "...【系统繁忙,内容已简化】";
            }
        }
        return content;
    }
}

七、总结与展望

系统通知作为连接用户与系统的重要桥梁,其设计质量直接影响用户体验和系统可靠性。本文从架构设计、数据模型、核心实现、高级特性到最佳实践,全面讲解了如何构建一个高可用、可扩展的通知系统。

相关推荐
Javatutouhouduan1 小时前
国内大厂Java面试高频题库(2026突击版)
java·架构师·java面试·java面试题·后端开发·java程序员·java八股文
Tim_101 小时前
【C++】017、new/delete与malloc/free的区别
java·数据结构·算法
青山木2 小时前
一把 Redis 分布式锁,踩透四个坑:锁争抢、僵尸锁、锁过期、锁丢失
java·数据库·redis·后端
Java内核笔记2 小时前
万字避坑!Spring Boot 3.x 升 4.0 最全升级指南(附 Migration Checklist,建议收藏⭐)
java·后端
cfm_29143 小时前
SpringBoot启动原理
java·spring boot·spring
漫随流水3 小时前
Java——springboot web案例
java·开发语言·spring boot
从此以后自律4 小时前
Java Object 类常用方法全讲解
java·开发语言
念何架构之路4 小时前
Docker 容器状态机 学习
java·学习·docker
你有我备注吗4 小时前
SQL学习之查询
java·sql·学习
xixi09245 小时前
JMeter5.6.3基础使用教程1
java·开发语言