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

引言:被低估的系统通知

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

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

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

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

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;
    }
}

七、总结与展望

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

相关推荐
SimonKing4 小时前
🐔老乡鸡把菜谱在GitHub开源了,还说要给程序员发会员卡
java·后端·程序员
Q_Q19632884754 小时前
python+django/flask二手物品交易系统 二手商品发布 分类浏览 在线沟通与订单管理系统java+nodejs
java·spring boot·python·django·flask·node.js·php
yujkss5 小时前
23种设计模式之【责任链模式】-核心原理与 Java 实践
java·设计模式·责任链模式
88Ra5 小时前
小程序中获取年月日时分的组件
java·小程序·apache
半梦半醒*5 小时前
在Linux中部署tomcat
java·linux·运维·服务器·centos·tomcat
缘的猿5 小时前
Kubernetes 四层负载均衡:Service核心原理与实战指南
java·kubernetes·负载均衡
鼠鼠我捏,要死了捏5 小时前
Java Stream API性能优化实践指南
java·performance·stream api
王嘉俊9255 小时前
Java面试宝典:核心基础知识精讲
java·开发语言·面试·java基础·八股文
ZNineSun5 小时前
第二章:Java到Go的思维转变
java·golang