866 lines
28 KiB
Plaintext
866 lines
28 KiB
Plaintext
# 消息通知系统
|
|
|
|
## 通知系统架构
|
|
|
|
### 通知类型
|
|
- **站内信**: 系统内部消息通知
|
|
- **邮件通知**: 电子邮件发送
|
|
- **短信通知**: 手机短信发送
|
|
- **微信通知**: 微信公众号/小程序推送
|
|
- **系统公告**: 全站公告通知
|
|
|
|
### 核心模块
|
|
- **通知模板**: 消息模板管理
|
|
- **通知渠道**: 发送渠道配置
|
|
- **通知日志**: 发送记录和状态
|
|
- **通知队列**: 异步消息处理
|
|
|
|
## 站内信系统
|
|
|
|
### 站内信表结构
|
|
```sql
|
|
-- 站内信模板表
|
|
CREATE TABLE system_notify_template (
|
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
|
name VARCHAR(63) NOT NULL COMMENT '模板名称',
|
|
code VARCHAR(64) NOT NULL COMMENT '模板编码',
|
|
nickname VARCHAR(255) NOT NULL COMMENT '发送人名称',
|
|
content TEXT NOT NULL COMMENT '模板内容',
|
|
type TINYINT NOT NULL COMMENT '类型',
|
|
params VARCHAR(255) DEFAULT NULL COMMENT '参数数组',
|
|
status TINYINT NOT NULL COMMENT '开启状态',
|
|
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
|
|
|
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
|
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号'
|
|
);
|
|
|
|
-- 站内信消息表
|
|
CREATE TABLE system_notify_message (
|
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
|
user_id BIGINT NOT NULL COMMENT '用户id',
|
|
user_type TINYINT NOT NULL DEFAULT 0 COMMENT '用户类型',
|
|
template_id BIGINT NOT NULL COMMENT '模版编号',
|
|
template_code VARCHAR(64) NOT NULL COMMENT '模板编码',
|
|
template_nickname VARCHAR(63) NOT NULL COMMENT '模版发送人名称',
|
|
template_content TEXT NOT NULL COMMENT '模版内容',
|
|
template_type TINYINT NOT NULL COMMENT '模版类型',
|
|
template_params VARCHAR(255) NOT NULL COMMENT '模版参数',
|
|
read_status BIT NOT NULL COMMENT '是否已读',
|
|
read_time DATETIME DEFAULT NULL COMMENT '阅读时间',
|
|
|
|
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
|
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号'
|
|
);
|
|
```
|
|
|
|
### 站内信服务
|
|
```java
|
|
@Service
|
|
public class NotifyMessageServiceImpl implements NotifyMessageService {
|
|
|
|
@Resource
|
|
private NotifyMessageMapper notifyMessageMapper;
|
|
|
|
@Resource
|
|
private NotifyTemplateService notifyTemplateService;
|
|
|
|
@Override
|
|
public Long createNotifyMessage(Long userId, Integer userType,
|
|
NotifyTemplateReqDTO templateReq) {
|
|
// 获取消息模板
|
|
NotifyTemplateDO template = notifyTemplateService.getNotifyTemplate(templateReq.getTemplateCode());
|
|
|
|
// 渲染模板内容
|
|
String content = renderTemplateContent(template, templateReq.getTemplateParams());
|
|
|
|
// 创建消息
|
|
NotifyMessageDO message = new NotifyMessageDO();
|
|
message.setUserId(userId);
|
|
message.setUserType(userType);
|
|
message.setTemplateId(template.getId());
|
|
message.setTemplateCode(template.getCode());
|
|
message.setTemplateNickname(template.getNickname());
|
|
message.setTemplateContent(content);
|
|
message.setTemplateType(template.getType());
|
|
message.setTemplateParams(JsonUtils.toJsonString(templateReq.getTemplateParams()));
|
|
message.setReadStatus(false);
|
|
|
|
notifyMessageMapper.insert(message);
|
|
return message.getId();
|
|
}
|
|
|
|
@Override
|
|
public PageResult<NotifyMessageDO> getNotifyMessagePage(Long userId,
|
|
NotifyMessagePageReqVO pageReqVO) {
|
|
return notifyMessageMapper.selectPage(userId, pageReqVO);
|
|
}
|
|
|
|
@Override
|
|
public void markAsRead(Long userId, Long messageId) {
|
|
NotifyMessageDO message = validateNotifyMessageExists(messageId);
|
|
|
|
// 校验用户权限
|
|
if (!Objects.equals(message.getUserId(), userId)) {
|
|
throw exception(NOTIFY_MESSAGE_NOT_BELONGS_TO_USER);
|
|
}
|
|
|
|
// 标记已读
|
|
if (!message.getReadStatus()) {
|
|
NotifyMessageDO updateObj = new NotifyMessageDO();
|
|
updateObj.setId(messageId);
|
|
updateObj.setReadStatus(true);
|
|
updateObj.setReadTime(new Date());
|
|
notifyMessageMapper.updateById(updateObj);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void markAllAsRead(Long userId) {
|
|
notifyMessageMapper.updateReadStatusByUserId(userId);
|
|
}
|
|
|
|
@Override
|
|
public Long getUnreadCount(Long userId) {
|
|
return notifyMessageMapper.selectUnreadCountByUserId(userId);
|
|
}
|
|
|
|
/**
|
|
* 渲染模板内容
|
|
*/
|
|
private String renderTemplateContent(NotifyTemplateDO template, Map<String, Object> params) {
|
|
String content = template.getContent();
|
|
|
|
// 使用占位符替换参数
|
|
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
|
String placeholder = "{" + entry.getKey() + "}";
|
|
String value = String.valueOf(entry.getValue());
|
|
content = content.replace(placeholder, value);
|
|
}
|
|
|
|
return content;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 站内信模板管理
|
|
```java
|
|
@Service
|
|
public class NotifyTemplateServiceImpl implements NotifyTemplateService {
|
|
|
|
@Override
|
|
public Long createNotifyTemplate(NotifyTemplateCreateReqVO reqVO) {
|
|
// 校验模板编码唯一性
|
|
validateTemplateCodeDuplicate(null, reqVO.getCode());
|
|
|
|
// 创建模板
|
|
NotifyTemplateDO template = NotifyTemplateConvert.INSTANCE.convert(reqVO);
|
|
template.setParams(buildTemplateParams(reqVO.getContent()));
|
|
notifyTemplateMapper.insert(template);
|
|
|
|
return template.getId();
|
|
}
|
|
|
|
@Override
|
|
public void updateNotifyTemplate(NotifyTemplateUpdateReqVO reqVO) {
|
|
// 校验模板存在
|
|
validateNotifyTemplateExists(reqVO.getId());
|
|
|
|
// 校验编码唯一性
|
|
validateTemplateCodeDuplicate(reqVO.getId(), reqVO.getCode());
|
|
|
|
// 更新模板
|
|
NotifyTemplateDO updateObj = NotifyTemplateConvert.INSTANCE.convert(reqVO);
|
|
updateObj.setParams(buildTemplateParams(reqVO.getContent()));
|
|
notifyTemplateMapper.updateById(updateObj);
|
|
}
|
|
|
|
/**
|
|
* 解析模板参数
|
|
*/
|
|
private String buildTemplateParams(String content) {
|
|
Set<String> params = new HashSet<>();
|
|
|
|
// 提取 {参数名} 格式的参数
|
|
Pattern pattern = Pattern.compile("\\{([^}]+)\\}");
|
|
Matcher matcher = pattern.matcher(content);
|
|
|
|
while (matcher.find()) {
|
|
params.add(matcher.group(1));
|
|
}
|
|
|
|
return JsonUtils.toJsonString(new ArrayList<>(params));
|
|
}
|
|
}
|
|
```
|
|
|
|
## 邮件通知系统
|
|
|
|
### 邮件配置
|
|
```yaml
|
|
spring:
|
|
mail:
|
|
host: smtp.qq.com
|
|
port: 587
|
|
username: your-email@qq.com
|
|
password: your-password
|
|
properties:
|
|
mail:
|
|
smtp:
|
|
auth: true
|
|
starttls:
|
|
enable: true
|
|
required: true
|
|
```
|
|
|
|
### 邮件模板管理
|
|
```sql
|
|
-- 邮件模板表
|
|
CREATE TABLE system_mail_template (
|
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
|
name VARCHAR(63) NOT NULL COMMENT '模板名称',
|
|
code VARCHAR(63) NOT NULL COMMENT '模板编码',
|
|
account_id BIGINT NOT NULL COMMENT '发送的邮箱账号编号',
|
|
nickname VARCHAR(255) DEFAULT NULL COMMENT '发送人名称',
|
|
title VARCHAR(255) NOT NULL COMMENT '邮件标题',
|
|
content TEXT NOT NULL COMMENT '邮件内容',
|
|
params VARCHAR(255) NOT NULL COMMENT '参数数组',
|
|
status TINYINT NOT NULL COMMENT '开启状态',
|
|
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
|
|
|
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
|
);
|
|
```
|
|
|
|
### 邮件发送服务
|
|
```java
|
|
@Service
|
|
public class MailSendServiceImpl implements MailSendService {
|
|
|
|
@Resource
|
|
private JavaMailSender mailSender;
|
|
|
|
@Resource
|
|
private MailTemplateService mailTemplateService;
|
|
|
|
@Resource
|
|
private MailLogService mailLogService;
|
|
|
|
@Override
|
|
@Async
|
|
public Long sendSingleMail(String mail, Long userId, Integer userType,
|
|
String templateCode, Map<String, Object> templateParams) {
|
|
// 检查邮箱格式
|
|
if (!Validator.isEmail(mail)) {
|
|
throw exception(MAIL_SEND_MAIL_NOT_VALID, mail);
|
|
}
|
|
|
|
// 获取邮件模板
|
|
MailTemplateDO template = mailTemplateService.getMailTemplateByCodeFromCache(templateCode);
|
|
|
|
// 渲染邮件内容
|
|
String title = renderTemplateContent(template.getTitle(), templateParams);
|
|
String content = renderTemplateContent(template.getContent(), templateParams);
|
|
|
|
// 创建发送日志
|
|
Long sendLogId = mailLogService.createMailLog(userId, userType, mail,
|
|
template, templateParams);
|
|
|
|
try {
|
|
// 发送邮件
|
|
doSendMail(template, mail, title, content);
|
|
|
|
// 更新发送状态
|
|
mailLogService.updateMailSendResult(sendLogId, true, null);
|
|
|
|
} catch (Exception e) {
|
|
// 更新发送失败状态
|
|
mailLogService.updateMailSendResult(sendLogId, false, e.getMessage());
|
|
log.error("[sendSingleMail][发送邮件失败, mail:{}, template:{}]", mail, templateCode, e);
|
|
throw exception(MAIL_SEND_ERROR);
|
|
}
|
|
|
|
return sendLogId;
|
|
}
|
|
|
|
@Override
|
|
@Async
|
|
public void sendBatchMail(List<String> mails, String templateCode,
|
|
Map<String, Object> templateParams) {
|
|
for (String mail : mails) {
|
|
try {
|
|
sendSingleMail(mail, null, null, templateCode, templateParams);
|
|
} catch (Exception e) {
|
|
log.error("[sendBatchMail][发送邮件失败, mail:{}, template:{}]", mail, templateCode, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 执行邮件发送
|
|
*/
|
|
private void doSendMail(MailTemplateDO template, String toMail,
|
|
String title, String content) throws Exception {
|
|
// 获取邮箱账号配置
|
|
MailAccountDO account = mailAccountService.getMailAccount(template.getAccountId());
|
|
|
|
// 创建邮件消息
|
|
MimeMessage message = mailSender.createMimeMessage();
|
|
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
|
|
// 设置发件人
|
|
helper.setFrom(account.getMail(), template.getNickname());
|
|
|
|
// 设置收件人
|
|
helper.setTo(toMail);
|
|
|
|
// 设置邮件主题和内容
|
|
helper.setSubject(title);
|
|
helper.setText(content, true); // true 表示支持 HTML
|
|
|
|
// 发送邮件
|
|
mailSender.send(message);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 短信通知系统
|
|
|
|
### 短信渠道配置
|
|
```sql
|
|
-- 短信渠道表
|
|
CREATE TABLE system_sms_channel (
|
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
|
signature VARCHAR(12) NOT NULL COMMENT '短信签名',
|
|
code VARCHAR(63) NOT NULL COMMENT '渠道编码',
|
|
status TINYINT NOT NULL COMMENT '开启状态',
|
|
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
|
api_key VARCHAR(128) NOT NULL COMMENT '短信 API 的账号',
|
|
api_secret VARCHAR(128) DEFAULT NULL COMMENT '短信 API 的密钥',
|
|
callback_url VARCHAR(255) DEFAULT NULL COMMENT '短信发送回调 URL',
|
|
|
|
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
|
);
|
|
|
|
-- 短信模板表
|
|
CREATE TABLE system_sms_template (
|
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
|
type TINYINT NOT NULL COMMENT '短信签名',
|
|
status TINYINT NOT NULL COMMENT '开启状态',
|
|
code VARCHAR(63) NOT NULL COMMENT '模板编码',
|
|
name VARCHAR(63) NOT NULL COMMENT '模板名称',
|
|
content VARCHAR(255) NOT NULL COMMENT '模板内容',
|
|
params VARCHAR(255) NOT NULL COMMENT '参数数组',
|
|
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
|
api_template_id VARCHAR(63) NOT NULL COMMENT '短信 API 的模板编号',
|
|
channel_id BIGINT NOT NULL COMMENT '短信渠道编号',
|
|
channel_code VARCHAR(63) NOT NULL COMMENT '短信渠道编码',
|
|
|
|
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
|
);
|
|
```
|
|
|
|
### 短信发送服务
|
|
```java
|
|
@Service
|
|
public class SmsSendServiceImpl implements SmsSendService {
|
|
|
|
@Resource
|
|
private SmsTemplateService smsTemplateService;
|
|
|
|
@Resource
|
|
private SmsLogService smsLogService;
|
|
|
|
@Resource
|
|
private SmsClientFactory smsClientFactory;
|
|
|
|
@Override
|
|
@Async
|
|
public Long sendSingleSms(String mobile, Long userId, Integer userType,
|
|
String templateCode, Map<String, Object> templateParams) {
|
|
// 校验手机号码格式
|
|
if (!Validator.isMobile(mobile)) {
|
|
throw exception(SMS_SEND_MOBILE_NOT_VALID, mobile);
|
|
}
|
|
|
|
// 获取短信模板
|
|
SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode);
|
|
|
|
// 创建发送日志
|
|
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType,
|
|
template, templateParams);
|
|
|
|
try {
|
|
// 执行发送
|
|
SmsClient smsClient = smsClientFactory.getSmsClient(template.getChannelId());
|
|
SmsSendResult sendResult = smsClient.sendSms(mobile, template.getApiTemplateId(),
|
|
templateParams);
|
|
|
|
// 更新发送结果
|
|
smsLogService.updateSmsSendResult(sendLogId, sendResult);
|
|
|
|
} catch (Exception e) {
|
|
log.error("[sendSingleSms][发送短信失败, mobile:{}, template:{}]", mobile, templateCode, e);
|
|
smsLogService.updateSmsSendResult(sendLogId, false, e.getMessage());
|
|
throw exception(SMS_SEND_ERROR);
|
|
}
|
|
|
|
return sendLogId;
|
|
}
|
|
|
|
@Override
|
|
@Async
|
|
public void sendBatchSms(List<String> mobiles, String templateCode,
|
|
Map<String, Object> templateParams) {
|
|
for (String mobile : mobiles) {
|
|
try {
|
|
sendSingleSms(mobile, null, null, templateCode, templateParams);
|
|
} catch (Exception e) {
|
|
log.error("[sendBatchSms][发送短信失败, mobile:{}, template:{}]", mobile, templateCode, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void receiveSmsStatus(String channelCode, String text, HttpServletRequest request)
|
|
throws Throwable {
|
|
// 获取短信客户端
|
|
SmsClient smsClient = smsClientFactory.getSmsClient(channelCode);
|
|
|
|
// 解析回调数据
|
|
List<SmsReceiveRespDTO> receiveResults = smsClient.parseSmsReceiveStatus(text, request);
|
|
|
|
// 更新发送状态
|
|
for (SmsReceiveRespDTO result : receiveResults) {
|
|
smsLogService.updateSmsReceiveResult(result.getLogId(), result);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 短信客户端抽象
|
|
```java
|
|
public interface SmsClient {
|
|
|
|
/**
|
|
* 获得渠道编号
|
|
*/
|
|
String getChannelCode();
|
|
|
|
/**
|
|
* 发送短信
|
|
*/
|
|
SmsSendRespDTO sendSms(String mobile, String templateId,
|
|
Map<String, Object> templateParams) throws Throwable;
|
|
|
|
/**
|
|
* 解析接收短信的结果
|
|
*/
|
|
List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text,
|
|
HttpServletRequest request) throws Throwable;
|
|
}
|
|
|
|
@Component
|
|
public abstract class AbstractSmsClient implements SmsClient {
|
|
|
|
protected final SmsChannelProperties properties;
|
|
|
|
public AbstractSmsClient(SmsChannelProperties properties) {
|
|
this.properties = properties;
|
|
}
|
|
|
|
/**
|
|
* 初始化
|
|
*/
|
|
@PostConstruct
|
|
public final void init() {
|
|
doInit();
|
|
log.info("[init][配置({}) 初始化完成]", properties);
|
|
}
|
|
|
|
/**
|
|
* 自定义初始化
|
|
*/
|
|
protected abstract void doInit();
|
|
}
|
|
```
|
|
|
|
## 微信通知系统
|
|
|
|
### 微信模板消息
|
|
```java
|
|
@Service
|
|
public class WechatNotifyService {
|
|
|
|
@Resource
|
|
private WxMpService wxMpService;
|
|
|
|
/**
|
|
* 发送模板消息
|
|
*/
|
|
public void sendTemplateMessage(String openId, String templateId,
|
|
Map<String, Object> data, String url) {
|
|
try {
|
|
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
|
|
.toUser(openId)
|
|
.templateId(templateId)
|
|
.url(url)
|
|
.build();
|
|
|
|
// 设置模板数据
|
|
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
|
templateMessage.addData(new WxMpTemplateData(
|
|
entry.getKey(),
|
|
String.valueOf(entry.getValue()),
|
|
"#173177"
|
|
));
|
|
}
|
|
|
|
// 发送消息
|
|
wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
|
|
|
|
} catch (Exception e) {
|
|
log.error("[sendTemplateMessage][发送微信模板消息失败, openId:{}, templateId:{}]",
|
|
openId, templateId, e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送小程序订阅消息
|
|
*/
|
|
public void sendSubscribeMessage(String openId, String templateId,
|
|
Map<String, Object> data, String page) {
|
|
try {
|
|
WxMaSubscribeMessage subscribeMessage = WxMaSubscribeMessage.builder()
|
|
.toUser(openId)
|
|
.templateId(templateId)
|
|
.page(page)
|
|
.build();
|
|
|
|
// 设置数据
|
|
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
|
subscribeMessage.addData(new WxMaSubscribeData(
|
|
entry.getKey(),
|
|
String.valueOf(entry.getValue())
|
|
));
|
|
}
|
|
|
|
// 发送消息
|
|
wxMaService.getMsgService().sendSubscribeMsg(subscribeMessage);
|
|
|
|
} catch (Exception e) {
|
|
log.error("[sendSubscribeMessage][发送小程序订阅消息失败, openId:{}, templateId:{}]",
|
|
openId, templateId, e);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 系统公告
|
|
|
|
### 公告管理
|
|
```sql
|
|
-- 通知公告表
|
|
CREATE TABLE system_notice (
|
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '公告ID',
|
|
title VARCHAR(50) NOT NULL COMMENT '公告标题',
|
|
content TEXT NOT NULL COMMENT '公告内容',
|
|
type TINYINT NOT NULL COMMENT '公告类型',
|
|
status TINYINT NOT NULL DEFAULT 0 COMMENT '公告状态',
|
|
|
|
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
|
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号'
|
|
);
|
|
```
|
|
|
|
### 公告服务
|
|
```java
|
|
@Service
|
|
public class NoticeServiceImpl implements NoticeService {
|
|
|
|
@Override
|
|
public Long createNotice(NoticeCreateReqVO reqVO) {
|
|
// 创建通知公告
|
|
NoticeDO notice = NoticeConvert.INSTANCE.convert(reqVO);
|
|
noticeMapper.insert(notice);
|
|
|
|
// 推送给所有在线用户(可选)
|
|
if (Objects.equals(reqVO.getType(), NoticeTypeEnum.NOTICE.getType())) {
|
|
pushNoticeToAllUsers(notice);
|
|
}
|
|
|
|
return notice.getId();
|
|
}
|
|
|
|
@Override
|
|
public void updateNotice(NoticeUpdateReqVO reqVO) {
|
|
// 校验公告存在
|
|
validateNoticeExists(reqVO.getId());
|
|
|
|
// 更新公告
|
|
NoticeDO updateObj = NoticeConvert.INSTANCE.convert(reqVO);
|
|
noticeMapper.updateById(updateObj);
|
|
}
|
|
|
|
/**
|
|
* 推送公告给所有用户
|
|
*/
|
|
private void pushNoticeToAllUsers(NoticeDO notice) {
|
|
// 获取所有在线用户
|
|
Set<Long> onlineUserIds = getOnlineUserIds();
|
|
|
|
// 创建站内信
|
|
Map<String, Object> templateParams = new HashMap<>();
|
|
templateParams.put("title", notice.getTitle());
|
|
templateParams.put("content", notice.getContent());
|
|
|
|
for (Long userId : onlineUserIds) {
|
|
try {
|
|
notifyMessageService.createNotifyMessage(userId, UserTypeEnum.ADMIN.getValue(),
|
|
new NotifyTemplateReqDTO("SYSTEM_NOTICE", templateParams));
|
|
} catch (Exception e) {
|
|
log.error("[pushNoticeToAllUsers][推送公告失败, userId:{}, noticeId:{}]",
|
|
userId, notice.getId(), e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 消息队列集成
|
|
|
|
### 异步消息处理
|
|
```java
|
|
@Component
|
|
public class NotificationProducer {
|
|
|
|
@Resource
|
|
private RedisTemplate<String, Object> redisTemplate;
|
|
|
|
/**
|
|
* 发送站内信消息
|
|
*/
|
|
public void sendNotifyMessage(NotifyMessageSendDTO sendDTO) {
|
|
redisTemplate.convertAndSend("notify.message.send", sendDTO);
|
|
}
|
|
|
|
/**
|
|
* 发送邮件消息
|
|
*/
|
|
public void sendMailMessage(MailSendDTO sendDTO) {
|
|
redisTemplate.convertAndSend("mail.send", sendDTO);
|
|
}
|
|
|
|
/**
|
|
* 发送短信消息
|
|
*/
|
|
public void sendSmsMessage(SmsSendDTO sendDTO) {
|
|
redisTemplate.convertAndSend("sms.send", sendDTO);
|
|
}
|
|
}
|
|
|
|
@Component
|
|
public class NotificationConsumer {
|
|
|
|
@Resource
|
|
private NotifyMessageService notifyMessageService;
|
|
|
|
@Resource
|
|
private MailSendService mailSendService;
|
|
|
|
@Resource
|
|
private SmsSendService smsSendService;
|
|
|
|
@EventListener
|
|
public void onNotifyMessage(NotifyMessageSendDTO sendDTO) {
|
|
try {
|
|
notifyMessageService.createNotifyMessage(
|
|
sendDTO.getUserId(),
|
|
sendDTO.getUserType(),
|
|
sendDTO.getTemplateReq()
|
|
);
|
|
} catch (Exception e) {
|
|
log.error("[onNotifyMessage][处理站内信消息失败]", e);
|
|
}
|
|
}
|
|
|
|
@EventListener
|
|
public void onMailMessage(MailSendDTO sendDTO) {
|
|
try {
|
|
mailSendService.sendSingleMail(
|
|
sendDTO.getMail(),
|
|
sendDTO.getUserId(),
|
|
sendDTO.getUserType(),
|
|
sendDTO.getTemplateCode(),
|
|
sendDTO.getTemplateParams()
|
|
);
|
|
} catch (Exception e) {
|
|
log.error("[onMailMessage][处理邮件消息失败]", e);
|
|
}
|
|
}
|
|
|
|
@EventListener
|
|
public void onSmsMessage(SmsSendDTO sendDTO) {
|
|
try {
|
|
smsSendService.sendSingleSms(
|
|
sendDTO.getMobile(),
|
|
sendDTO.getUserId(),
|
|
sendDTO.getUserType(),
|
|
sendDTO.getTemplateCode(),
|
|
sendDTO.getTemplateParams()
|
|
);
|
|
} catch (Exception e) {
|
|
log.error("[onSmsMessage][处理短信消息失败]", e);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 通知统一发送
|
|
|
|
### 通知发送门面
|
|
```java
|
|
@Service
|
|
public class NotificationFacadeService {
|
|
|
|
@Resource
|
|
private NotifyMessageService notifyMessageService;
|
|
|
|
@Resource
|
|
private MailSendService mailSendService;
|
|
|
|
@Resource
|
|
private SmsSendService smsSendService;
|
|
|
|
@Resource
|
|
private WechatNotifyService wechatNotifyService;
|
|
|
|
@Resource
|
|
private NotificationProducer notificationProducer;
|
|
|
|
/**
|
|
* 发送通知(多渠道)
|
|
*/
|
|
public void sendNotification(NotificationSendDTO sendDTO) {
|
|
// 站内信
|
|
if (sendDTO.getChannels().contains(NotifyChannelEnum.SITE_MESSAGE)) {
|
|
sendSiteMessage(sendDTO);
|
|
}
|
|
|
|
// 邮件
|
|
if (sendDTO.getChannels().contains(NotifyChannelEnum.EMAIL)) {
|
|
sendEmail(sendDTO);
|
|
}
|
|
|
|
// 短信
|
|
if (sendDTO.getChannels().contains(NotifyChannelEnum.SMS)) {
|
|
sendSms(sendDTO);
|
|
}
|
|
|
|
// 微信
|
|
if (sendDTO.getChannels().contains(NotifyChannelEnum.WECHAT)) {
|
|
sendWechat(sendDTO);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 批量发送通知
|
|
*/
|
|
@Async
|
|
public void sendBatchNotification(List<NotificationSendDTO> sendDTOs) {
|
|
for (NotificationSendDTO sendDTO : sendDTOs) {
|
|
try {
|
|
sendNotification(sendDTO);
|
|
} catch (Exception e) {
|
|
log.error("[sendBatchNotification][批量发送通知失败]", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void sendSiteMessage(NotificationSendDTO sendDTO) {
|
|
NotifyMessageSendDTO messageDTO = new NotifyMessageSendDTO();
|
|
messageDTO.setUserId(sendDTO.getUserId());
|
|
messageDTO.setUserType(sendDTO.getUserType());
|
|
messageDTO.setTemplateReq(new NotifyTemplateReqDTO(
|
|
sendDTO.getTemplateCode(),
|
|
sendDTO.getTemplateParams()
|
|
));
|
|
|
|
notificationProducer.sendNotifyMessage(messageDTO);
|
|
}
|
|
|
|
private void sendEmail(NotificationSendDTO sendDTO) {
|
|
if (StrUtil.isBlank(sendDTO.getEmail())) {
|
|
return;
|
|
}
|
|
|
|
MailSendDTO mailDTO = new MailSendDTO();
|
|
mailDTO.setMail(sendDTO.getEmail());
|
|
mailDTO.setUserId(sendDTO.getUserId());
|
|
mailDTO.setUserType(sendDTO.getUserType());
|
|
mailDTO.setTemplateCode(sendDTO.getTemplateCode());
|
|
mailDTO.setTemplateParams(sendDTO.getTemplateParams());
|
|
|
|
notificationProducer.sendMailMessage(mailDTO);
|
|
}
|
|
|
|
private void sendSms(NotificationSendDTO sendDTO) {
|
|
if (StrUtil.isBlank(sendDTO.getMobile())) {
|
|
return;
|
|
}
|
|
|
|
SmsSendDTO smsDTO = new SmsSendDTO();
|
|
smsDTO.setMobile(sendDTO.getMobile());
|
|
smsDTO.setUserId(sendDTO.getUserId());
|
|
smsDTO.setUserType(sendDTO.getUserType());
|
|
smsDTO.setTemplateCode(sendDTO.getTemplateCode());
|
|
smsDTO.setTemplateParams(sendDTO.getTemplateParams());
|
|
|
|
notificationProducer.sendSmsMessage(smsDTO);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 最佳实践
|
|
|
|
### 性能优化
|
|
1. **异步发送**: 使用消息队列异步处理通知
|
|
2. **批量发送**: 支持批量发送减少系统开销
|
|
3. **失败重试**: 实现发送失败的重试机制
|
|
4. **流量控制**: 设置发送频率限制防止滥用
|
|
|
|
### 用户体验
|
|
1. **消息分类**: 不同类型消息使用不同模板
|
|
2. **个性化**: 支持用户自定义通知偏好设置
|
|
3. **及时性**: 重要消息实时推送
|
|
4. **历史记录**: 保留通知历史方便用户查看
|
|
|
|
### 监控告警
|
|
1. **发送成功率**: 监控各渠道发送成功率
|
|
2. **延迟监控**: 监控消息发送延迟
|
|
3. **失败分析**: 分析发送失败原因
|
|
4. **用量统计**: 统计各类型消息发送量 |