This commit is contained in:
那些花儿
2025-09-12 17:54:47 +08:00
parent c27136f894
commit efc7f9ceb2
11 changed files with 756 additions and 30 deletions
@@ -1,24 +0,0 @@
package com.budwk.app.sys.controller.message;
import com.budwk.app.sys.models.Sys_user;
/**
* 消息发送者
*/
public interface SysMessageSender {
/**
* 发送消息
* @param message 消息内容
* @param receiver 接收人
* @return 发送结果
*/
// boolean send(Message message, Sys_user receiver);
/**
* 支持的渠道类型
* @return 渠道类型
*/
String getChannelType();
}
@@ -1,4 +0,0 @@
package com.budwk.app.sys.controller.message;
public class SysSmsMessageSender {
}
@@ -0,0 +1,27 @@
package com.budwk.app.zhgh.dayofficework.message.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 消息通道
*/
@Getter
@AllArgsConstructor
public enum GlobalMessageChannel {
LOCAL("本地"),
WECHAT("微信"),
EMAIL("邮件"),
SMS("短信"),
DINGDING("钉钉"),
WECHAT_ENTERPRISE("企业微信");
private final String value;
}
@@ -0,0 +1,48 @@
package com.budwk.app.zhgh.dayofficework.message.mode;
import cn.hutool.json.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 消息发送请求
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GlobalMessageSendRequest {
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 附件
*/
private List<JSONObject> attachments;
/**
* 消息类型(1:系统通知 2:待办通知)
*/
private Integer type;
/**
* 配置
*/
private JSONObject config;
/**
* 接收人ID
*/
private List<String> receiverIds;
}
@@ -0,0 +1,89 @@
package com.budwk.app.zhgh.dayofficework.message.models;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.List;
@Table("global_message")
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("全局消息")
public class GlobalMessage extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("标题")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String title;
@Column
@Comment("内容")
@ColDefine(type = ColType.TEXT)
private String content;
@Column
@Comment("附件")
@ColDefine(type = ColType.MYSQL_JSON)
private List<JSONObject> attachments;
@Column
@Comment("消息类型(1:系统公告 2:消息通知)")
@ColDefine(type = ColType.INT)
private Integer type;
@Column
@Comment("发送人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String senderId;
@Column
@Comment("发送人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String senderName;
@Column
@Comment("发送人工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String senderLoginName;
// @Column
// @Comment("消息通道(用于第三方消息)")
// @ColDefine(type = ColType.MYSQL_JSON)
// private List<String> channels;
@Column
@Comment("扩展字段")
@ColDefine(type = ColType.MYSQL_JSON)
private JSONObject ext;
@Column
@Comment("状态(1:草稿 2:已发送 3:已撤回 4:已删除)")
@ColDefine(type = ColType.INT)
private Integer status;
@Column
@Comment("发送成功")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean sendSuccess;
@Column
@Comment("发送结果")
@ColDefine(type = ColType.MYSQL_JSON)
private String sendResult;
}
@@ -0,0 +1,59 @@
package com.budwk.app.zhgh.dayofficework.message.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
@Table("global_message_receiver")
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("全局消息接收用户")
public class GlobalMessageReceiver extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("消息ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String messageId;
@Column
@Comment("接收人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String receiverId;
@Column
@Comment("接收人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String receiverName;
@Column
@Comment("接收人工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String receiverLoginName;
@Column
@Comment("阅读状态")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean isRead;
@Column
@Comment("阅读时间")
@ColDefine(type = ColType.DATETIME)
private Date readTime;
}
@@ -0,0 +1,228 @@
package com.budwk.app.zhgh.dayofficework.message.service;
import cn.hutool.json.JSONObject;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import com.budwk.app.zhgh.dayofficework.message.mode.GlobalMessageSendRequest;
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.Ioc;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 全局消息发送服务
*/
@IocBean(create = "initStrategies")
@Slf4j
public class GlobalMessageSendService {
@Inject
private Dao dao;
@Inject
private Ioc ioc;
/**
* 消息发送策略缓存
*/
private final Map<GlobalMessageChannel, GlobalMessageSendStrategy> strategyMap = new ConcurrentHashMap<>();
/**
* 本地消息策略(必须存在)
*/
private GlobalMessageSendStrategy localStrategy;
/**
* 初始化策略
*/
public void initStrategies() {
try {
log.info("开始初始化消息发送策略...");
// 获取所有实现了GlobalMessageSendStrategy接口的Bean
String[] beanNames = ioc.getNames();
for (String beanName : beanNames) {
try {
Object bean = ioc.get(null, beanName);
if (bean instanceof GlobalMessageSendStrategy) {
GlobalMessageSendStrategy strategy = (GlobalMessageSendStrategy) bean;
GlobalMessageChannel channel = strategy.getChannel();
strategyMap.put(channel, strategy);
// 缓存本地策略
if (channel == GlobalMessageChannel.LOCAL) {
localStrategy = strategy;
}
log.info("注册消息发送策略:{} -> {}", channel.getValue(), strategy.getClass().getSimpleName());
}
} catch (Exception e) {
log.warn("初始化Bean {}时出错:{}", beanName, e.getMessage());
}
}
if (localStrategy == null) {
throw new RuntimeException("本地消息发送策略未找到,系统无法正常工作");
}
log.info("消息发送策略初始化完成,共注册{}个策略", strategyMap.size());
} catch (Exception e) {
log.error("消息发送策略初始化失败", e);
throw new RuntimeException("消息发送策略初始化失败", e);
}
}
/**
* 发送消息(简化API
*/
public void sendMessage(String title, String content, List<String> receiverIds) {
sendMessage(title, content, 1, receiverIds, null);
}
/**
* 发送消息(完整API
*/
public void sendMessage(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
GlobalMessageSendRequest request = new GlobalMessageSendRequest();
request.setTitle(title);
request.setContent(content);
request.setType(type);
request.setReceiverIds(receiverIds);
request.setConfig(config);
sendMessage(request);
}
/**
* 发送消息(使用请求对象)
*/
public void sendMessage(GlobalMessageSendRequest request) {
try {
log.info("开始发送全局消息,标题:{},接收人数量:{}", request.getTitle(),
request.getReceiverIds() != null ? request.getReceiverIds().size() : 0);
// 参数校验
validateRequest(request);
// 获取启用的策略
List<GlobalMessageSendStrategy> enabledStrategies = getEnabledStrategies();
if (enabledStrategies.isEmpty()) {
log.warn("没有找到启用的消息发送策略");
return;
}
// 执行消息发送 - 本地消息优先,然后是外部策略
for (GlobalMessageSendStrategy strategy : enabledStrategies) {
try {
// 本地消息必须同步发送,确保优先完成
if (strategy.getChannel() == GlobalMessageChannel.LOCAL) {
strategy.send(request.getTitle(), request.getContent(), request.getType(),
request.getReceiverIds(), request.getConfig());
log.info("本地消息发送成功");
} else {
// 外部消息策略根据配置决定同步或异步
if (strategy.supportAsync()) {
// 异步发送
CompletableFuture.runAsync(() -> {
try {
strategy.send(request.getTitle(), request.getContent(), request.getType(),
request.getReceiverIds(), request.getConfig());
log.info("异步发送消息成功,渠道:{}", strategy.getChannel().getValue());
} catch (Exception e) {
log.error("异步发送消息失败,渠道:{},错误:{}", strategy.getChannel().getValue(), e.getMessage(), e);
}
});
} else {
// 同步发送
strategy.send(request.getTitle(), request.getContent(), request.getType(),
request.getReceiverIds(), request.getConfig());
log.info("同步发送消息成功,渠道:{}", strategy.getChannel().getValue());
}
}
} catch (Exception e) {
log.error("发送消息失败,渠道:{},错误:{}", strategy.getChannel().getValue(), e.getMessage(), e);
// 本地消息发送失败时,记录严重错误并抛出异常
if (strategy.getChannel() == GlobalMessageChannel.LOCAL) {
log.error("本地消息发送失败,这可能影响消息的完整性");
throw e;
}
}
}
log.info("全局消息发送完成,标题:{}", request.getTitle());
} catch (Exception e) {
log.error("全局消息发送失败,标题:{},错误:{}", request.getTitle(), e.getMessage(), e);
throw new RuntimeException("消息发送失败:" + e.getMessage(), e);
}
}
/**
* 参数校验
*/
private void validateRequest(GlobalMessageSendRequest request) {
if (request == null) {
throw new IllegalArgumentException("消息发送请求不能为空");
}
if (request.getTitle() == null || request.getTitle().trim().isEmpty()) {
throw new IllegalArgumentException("消息标题不能为空");
}
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
throw new IllegalArgumentException("消息内容不能为空");
}
if (request.getReceiverIds() == null || request.getReceiverIds().isEmpty()) {
throw new IllegalArgumentException("接收人不能为空");
}
if (request.getType() == null) {
request.setType(1); // 默认为系统消息
}
}
/**
* 获取启用的发送策略
* 本地消息策略必须启用,其他策略根据isEnabled()方法判断
*/
private List<GlobalMessageSendStrategy> getEnabledStrategies() {
List<GlobalMessageSendStrategy> enabledStrategies = new ArrayList<>();
// 本地消息策略必须存在且优先添加
if (localStrategy != null) {
enabledStrategies.add(localStrategy);
log.debug("添加本地消息策略:{}", localStrategy.getChannel().getValue());
}
// 查找其他启用的策略
for (GlobalMessageSendStrategy strategy : strategyMap.values()) {
// 跳过本地策略(已经添加)
if (strategy.getChannel() == GlobalMessageChannel.LOCAL) {
continue;
}
// 检查策略是否启用
if (strategy.isEnabled()) {
enabledStrategies.add(strategy);
log.debug("添加启用的外部消息策略:{}", strategy.getChannel().getValue());
} else {
log.debug("跳过未启用的消息策略:{}", strategy.getChannel().getValue());
}
}
log.info("共找到{}个启用的消息发送策略", enabledStrategies.size());
return enabledStrategies;
}
/**
* 获取所有可用的发送策略
*/
public Map<GlobalMessageChannel, GlobalMessageSendStrategy> getAvailableStrategies() {
return new HashMap<>(strategyMap);
}
}
@@ -0,0 +1,55 @@
package com.budwk.app.zhgh.dayofficework.message.strategy;
import cn.hutool.json.JSONObject;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import java.util.List;
/**
* 全局消息发送策略
*/
public interface GlobalMessageSendStrategy {
/**
* 获取渠道类型
*/
GlobalMessageChannel getChannel();
/**
* 是否支持异步发送
*/
default boolean supportAsync() {
return true;
}
/**
* 是否启用该策略
* 默认启用,子类可以重写此方法来控制策略的启用状态
*/
default boolean isEnabled() {
return true;
}
/**
* 发送消息
*
* @param title 标题
* @param content 内容
* @param type 0:系统消息 1:待办消息
* @param receiverIds 接收人ID
* @param config 配置
*/
void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config);
/**
* 发送消息
*
* @param title 标题
* @param content 内容
* @param type 0:系统消息 1:待办消息
* @param receiverLoginNames 接收人登录名
* @param config 配置
*/
void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config);
}
@@ -0,0 +1,90 @@
package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
import cn.hutool.json.JSONObject;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import com.budwk.app.zhgh.dayofficework.message.models.GlobalMessage;
import com.budwk.app.zhgh.dayofficework.message.models.GlobalMessageReceiver;
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
/**
* 本地消息发送策略
*/
@IocBean
@Slf4j
public class LocalMessageSendStrategy implements GlobalMessageSendStrategy {
@Inject
private Dao dao;
@Override
public GlobalMessageChannel getChannel() {
return GlobalMessageChannel.LOCAL;
}
@Override
public boolean supportAsync() {
return false;
}
@Override
public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getId, "in", receiverIds));
sendMessage(title, content, type, users, config);
}
@Override
public void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config) {
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "in", receiverLoginNames));
sendMessage(title, content, type, users, config);
}
private void sendMessage(String title, String content, Integer type, List<Sys_user> receivers, JSONObject config) {
try {
log.info("开始发送本地消息,标题:{},接收人数量:{}", title, receivers.size());
// 消息主体
GlobalMessage globalMessage = new GlobalMessage();
globalMessage.setTitle(title);
globalMessage.setContent(content);
globalMessage.setType(type);
globalMessage.setSenderId(SecurityUtil.getUserId());
globalMessage.setSenderName(SecurityUtil.getUserUsername());
globalMessage.setSenderLoginName(SecurityUtil.getUserLoginname());
globalMessage.setStatus(1); // 草稿状态
globalMessage.setSendSuccess(false);
dao.insert(globalMessage);
// 消息接收人
List<GlobalMessageReceiver> receiverList = receivers.stream().map(r -> {
GlobalMessageReceiver receiver = new GlobalMessageReceiver();
receiver.setMessageId(globalMessage.getId());
receiver.setReceiverId(r.getId());
receiver.setReceiverName(r.getUsername()); // 修复:使用用户名而不是登录名
receiver.setReceiverLoginName(r.getLoginname());
receiver.setIsRead(false);
return receiver;
}).toList();
dao.insert(receiverList);
// 更新消息状态为已发送
globalMessage.setStatus(2);
globalMessage.setSendSuccess(true);
globalMessage.setSendResult("本地消息发送成功");
dao.update(globalMessage);
log.info("本地消息发送成功,消息ID:{},接收人数量:{}", globalMessage.getId(), receivers.size());
} catch (Exception e) {
log.error("本地消息发送失败,标题:{},错误信息:{}", title, e.getMessage(), e);
throw new RuntimeException("本地消息发送失败:" + e.getMessage(), e);
}
}
}
@@ -0,0 +1,130 @@
package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
import cn.hutool.json.JSONObject;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
/**
* 短信发送策略
*/
@IocBean
@Slf4j
public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
@Inject
private Dao dao;
@Override
public GlobalMessageChannel getChannel() {
return GlobalMessageChannel.SMS;
}
@Override
public boolean supportAsync() {
return true; // 短信发送支持异步
}
// @Override
// public boolean isEnabled() {
// return true;
// }
@Override
public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
try {
log.info("开始发送短信消息,标题:{},接收人数量:{}", title, receiverIds.size());
// 查询用户手机号
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getId, "in", receiverIds));
sendSmsMessage(title, content, type, users, config);
log.info("短信消息发送完成,标题:{},接收人数量:{}", title, users.size());
} catch (Exception e) {
log.error("短信消息发送失败,标题:{},错误:{}", title, e.getMessage(), e);
throw new RuntimeException("短信消息发送失败:" + e.getMessage(), e);
}
}
@Override
public void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config) {
try {
log.info("开始发送短信消息(按登录名),标题:{},接收人数量:{}", title, receiverLoginNames.size());
// 查询用户手机号
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "in", receiverLoginNames));
sendSmsMessage(title, content, type, users, config);
log.info("短信消息发送完成(按登录名),标题:{},接收人数量:{}", title, users.size());
} catch (Exception e) {
log.error("短信消息发送失败(按登录名),标题:{},错误:{}", title, e.getMessage(), e);
throw new RuntimeException("短信消息发送失败:" + e.getMessage(), e);
}
}
/**
* 发送短信消息
*/
private void sendSmsMessage(String title, String content, Integer type, List<Sys_user> users, JSONObject config) {
for (Sys_user user : users) {
try {
String mobile = user.getMobile();
if (mobile == null || mobile.trim().isEmpty()) {
log.warn("用户{}没有手机号,跳过短信发送", user.getUsername());
continue;
}
// 构建短信内容
String smsContent = buildSmsContent(title, content, type);
// 执行发送
boolean success = true;
if (success) {
log.info("短信发送成功:用户{},手机号:{}", user.getUsername(), mobile);
} else {
log.warn("短信发送失败:用户{},手机号:{}", user.getUsername(), mobile);
}
} catch (Exception e) {
log.error("发送短信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
}
}
}
/**
* 构建短信内容
*/
private String buildSmsContent(String title, String content, Integer type) {
StringBuilder sb = new StringBuilder();
// 根据消息类型添加前缀
if (type != null && type == 2) {
sb.append("【待办通知】");
} else {
sb.append("【系统通知】");
}
// 添加标题和内容
sb.append(title);
if (content != null && !content.trim().isEmpty()) {
sb.append("").append(content);
}
// 限制短信长度(一般短信限制70个字符)
String smsContent = sb.toString();
if (smsContent.length() > 67) {
smsContent = smsContent.substring(0, 67) + "...";
}
return smsContent;
}
}
@@ -6,18 +6,24 @@ import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import com.budwk.app.zhgh.staffbenefit.huimin.models.Huimin;
import com.budwk.app.zhgh.staffbenefit.huimin.service.HuiminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Arrays;
import java.util.List;
@IocBean
@At("/platform/huimin/manage")
@Api(("惠民服务管理"))
@@ -28,6 +34,9 @@ public class HuiminManageController {
@Inject
private HuiminService huiminService;
@Inject
private GlobalMessageSendService globalMessageSendService;
@At("")
@SaCheckPermission("huimin.manage")
@Ok("beetl:/platform/zhgh/staffbenefit/huimin/manage/index.html")
@@ -37,11 +46,30 @@ public class HuiminManageController {
@At
@SaCheckPermission("huimin.manage")
public Result pageData(PageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
t1.*
FROM
huimin t1
LEFT JOIN activity_user_scope aus ON t1.groupId = aus.groupId
AND aus.userId = @userId
$condition
""");
Cnd cnd = Cnd.NEW();
if(StrUtil.isNotBlank(pageForm.getSearchKeyword())){
cnd.where().andLike(Huimin::getTitle, pageForm.getSearchKeyword());
cnd.where().andLike("t1.title", "%" + pageForm.getSearchKeyword() + "%");
}
Pagination pagination = huiminService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
cnd.and(Cnd.exps("aus.groupId","is not",null).or("t1.groupId","is",null));
sql.setCondition(cnd);
Pagination pagination = huiminService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
String title = "审批待办";
String content = "您有一个新的审批任务需要处理,请及时登录系统查看。";
List<String> receiverIds = Arrays.asList("00198c31ee094466845979ab8dabf83b", "00266dbf9e014376ab76072851b1a8ac");
// 自动发送到所有启用的渠道
globalMessageSendService.sendMessage(title, content, 2, receiverIds, null);
return Result.success(pagination);
}