Commit 3a23a4cb by zhangxingmin

push

parent 6945ad0a
...@@ -24,13 +24,12 @@ public class ApiNotificationTaskController implements ApiNotificationTaskFeignCl ...@@ -24,13 +24,12 @@ public class ApiNotificationTaskController implements ApiNotificationTaskFeignCl
private ApiNotificationTaskService apiNotificationTaskService; private ApiNotificationTaskService apiNotificationTaskService;
/** /**
* 发送企业微信消息 * 通用发送接口(支持所有渠道)
* @param request * @param request
* @return * @return
*/ */
@Override public Result<ApiSendResponse> send(@RequestBody @Validated ApiSendRequest request) {
public Result<ApiSendResponse> sendWecomMessage(ApiSendRequest request) { return apiNotificationTaskService.send(request);
return apiNotificationTaskService.sendWecomMessage(request);
} }
// /** // /**
......
...@@ -5,5 +5,5 @@ import com.yd.notice.feign.request.ApiSendRequest; ...@@ -5,5 +5,5 @@ import com.yd.notice.feign.request.ApiSendRequest;
import com.yd.notice.feign.response.ApiSendResponse; import com.yd.notice.feign.response.ApiSendResponse;
public interface ApiNotificationTaskService { public interface ApiNotificationTaskService {
Result<ApiSendResponse> sendWecomMessage(ApiSendRequest request); Result<ApiSendResponse> send(ApiSendRequest request);
} }
...@@ -8,7 +8,6 @@ import com.yd.notice.service.service.INotificationTaskService; ...@@ -8,7 +8,6 @@ import com.yd.notice.service.service.INotificationTaskService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@Service @Service
...@@ -18,13 +17,12 @@ public class ApiNotificationTaskServiceImpl implements ApiNotificationTaskServic ...@@ -18,13 +17,12 @@ public class ApiNotificationTaskServiceImpl implements ApiNotificationTaskServic
private INotificationTaskService iNotificationTaskService; private INotificationTaskService iNotificationTaskService;
/** /**
* 发送企业微信消息 * 通用发送接口(支持所有渠道)
* @param request * @param request
* @return * @return
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) public Result<ApiSendResponse> send(ApiSendRequest request) {
public Result<ApiSendResponse> sendWecomMessage(ApiSendRequest request) {
String taskBizId = iNotificationTaskService.createAndSendTask( String taskBizId = iNotificationTaskService.createAndSendTask(
request.getChannelBizId(), request.getChannelBizId(),
request.getTemplateBizId(), request.getTemplateBizId(),
...@@ -36,5 +34,4 @@ public class ApiNotificationTaskServiceImpl implements ApiNotificationTaskServic ...@@ -36,5 +34,4 @@ public class ApiNotificationTaskServiceImpl implements ApiNotificationTaskServic
return Result.success(response); return Result.success(response);
} }
} }
...@@ -16,14 +16,13 @@ import org.springframework.web.bind.annotation.RequestBody; ...@@ -16,14 +16,13 @@ import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = "yd-notice-api",path = "/notice/api/notificationTask",fallbackFactory = ApiNotificationTaskFeignFallbackFactory.class) @FeignClient(name = "yd-notice-api",path = "/notice/api/notificationTask",fallbackFactory = ApiNotificationTaskFeignFallbackFactory.class)
public interface ApiNotificationTaskFeignClient { public interface ApiNotificationTaskFeignClient {
/** /**
* 发送企业微信消息 * 通用发送接口(支持所有渠道)
* @param request * @param request
* @return * @return
*/ */
@PostMapping("/send/wecom") @PostMapping("/send")
Result<ApiSendResponse> sendWecomMessage(@Validated @RequestBody ApiSendRequest request); Result<ApiSendResponse> send(@Validated @RequestBody ApiSendRequest request);
// /** // /**
// * 查询消息任务状态 // * 查询消息任务状态
......
...@@ -90,11 +90,28 @@ ...@@ -90,11 +90,28 @@
<version>4.6.0</version> <version>4.6.0</version>
</dependency> </dependency>
<!-- Redis 依赖(用于 AccessToken 缓存) --> <dependency>
<!-- <dependency>--> <groupId>com.sun.mail</groupId>
<!-- <groupId>org.springframework.boot</groupId>--> <artifactId>javax.mail</artifactId>
<!-- <artifactId>spring-boot-starter-data-redis</artifactId>--> <version>1.6.2</version>
<!-- </dependency>--> </dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
<version>0.3.6</version>
</dependency>
<!-- Hutool 工具类(可选) --> <!-- Hutool 工具类(可选) -->
<dependency> <dependency>
......
package com.yd.notice.service.send;
import com.alibaba.fastjson.JSONObject;
import com.yd.notice.service.model.ChannelConfig;
import com.yd.notice.service.model.NotificationTask;
import com.yd.notice.service.service.IChannelConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
@Slf4j
@Component
@RequiredArgsConstructor
public class EmailMessageSender implements MessageSender {
private final IChannelConfigService channelConfigService;
@Override
public SendResult send(NotificationTask task) {
try {
ChannelConfig config = channelConfigService.getByChannelBizId(task.getChannelBizId());
if (config == null || config.getStatus() != 1) {
return SendResult.failNonRetryable("CONFIG_NOT_EXIST", "渠道配置不存在或已禁用");
}
JSONObject emailConfig = JSONObject.parseObject(config.getConfigValue());
String host = emailConfig.getString("host");
int port = emailConfig.getInteger("port");
String username = emailConfig.getString("username");
String password = emailConfig.getString("password");
String from = emailConfig.getString("from");
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.ssl.enable", port == 465);
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
helper.setFrom(from);
helper.setTo(task.getReceiver());
helper.setSubject(task.getTitle());
helper.setText(task.getContent(), true); // true表示HTML内容
mailSender.send(mimeMessage);
log.info("邮件发送成功, taskBizId={}, receiver={}", task.getTaskBizId(), task.getReceiver());
return SendResult.success();
} catch (Exception e) {
log.error("邮件发送异常, taskBizId={}", task.getTaskBizId(), e);
// 邮件发送异常一般可重试(网络、超时等),但配置错误不可重试
if (e.getMessage() != null && e.getMessage().contains("Authentication failed")) {
return SendResult.failNonRetryable("AUTH_FAILED", e.getMessage());
}
return SendResult.failRetryable("SEND_EXCEPTION", e.getMessage());
}
}
@Override
public String getSupportedChannelType() {
return "email";
}
}
\ No newline at end of file
package com.yd.notice.service.send;
import com.yd.notice.service.model.NotificationTask;
/**
* 消息发送器接口
*/
public interface MessageSender {
/**
* 发送消息
* @param task 消息任务(含标题、内容、接收人、渠道ID等)
* @return 发送结果(成功/失败+是否可重试)
*/
SendResult send(NotificationTask task);
/**
* 支持的渠道类型,与 channel_config.channel_type 对应
* 例如:wecom, sms, email, wechat, miniprogram, app
*/
String getSupportedChannelType();
}
\ No newline at end of file
package com.yd.notice.service.send;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 发送器路由工厂
*/
@Slf4j
@Component
public class MessageSenderRouter {
private final Map<String, MessageSender> senderMap = new ConcurrentHashMap<>();
private final Map<String, MessageSender> senderCache = new ConcurrentHashMap<>();
// Spring 会自动注入所有 MessageSender 的实现 Bean
public MessageSenderRouter(Map<String, MessageSender> senders) {
for (MessageSender sender : senders.values()) {
String type = sender.getSupportedChannelType();
if (senderMap.containsKey(type)) {
log.warn("渠道类型 {} 重复注册,旧: {}, 新: {}", type, senderMap.get(type).getClass().getName(), sender.getClass().getName());
}
senderMap.put(type, sender);
log.info("注册消息发送器: {} -> {}", type, sender.getClass().getSimpleName());
}
}
public MessageSender getSender(String channelType) {
MessageSender sender = senderCache.get(channelType);
if (sender != null) {
return sender;
}
sender = senderMap.get(channelType);
if (sender == null) {
throw new IllegalArgumentException("不支持的渠道类型: " + channelType);
}
senderCache.put(channelType, sender);
return sender;
}
}
\ No newline at end of file
package com.yd.notice.service.send;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SendResult {
private boolean success;
private boolean retryable; // true: 可重试, false: 永久失败
private String errorCode;
private String errorMsg;
public static SendResult success() {
return new SendResult(true, false, null, null);
}
public static SendResult failRetryable(String errorCode, String errorMsg) {
return new SendResult(false, true, errorCode, errorMsg);
}
public static SendResult failNonRetryable(String errorCode, String errorMsg) {
return new SendResult(false, false, errorCode, errorMsg);
}
}
\ No newline at end of file
package com.yd.notice.service.send;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.yd.notice.service.model.ChannelConfig;
import com.yd.notice.service.model.NotificationTask;
import com.yd.notice.service.service.IChannelConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class SmsMessageSender implements MessageSender {
private final IChannelConfigService channelConfigService;
@Override
public SendResult send(NotificationTask task) {
try {
ChannelConfig config = channelConfigService.getByChannelBizId(task.getChannelBizId());
if (config == null || config.getStatus() != 1) {
return SendResult.failNonRetryable("CONFIG_NOT_EXIST", "渠道配置不存在或已禁用");
}
JSONObject smsConfig = JSONObject.parseObject(config.getConfigValue());
String accessKeyId = smsConfig.getString("accessKeyId");
String accessSecret = smsConfig.getString("accessSecret");
String signName = smsConfig.getString("signName");
String templateCode = smsConfig.getString("templateCode");
// 创建旧版客户端
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessSecret);
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(task.getReceiver());
request.setSignName(signName);
request.setTemplateCode(templateCode);
// 模板参数必须是 JSON 字符串,例如 {"code":"123456"}
request.setTemplateParam(task.getContent());
SendSmsResponse response = client.getAcsResponse(request);
String code = response.getCode();
if ("OK".equals(code)) {
log.info("短信发送成功, taskBizId={}, receiver={}", task.getTaskBizId(), task.getReceiver());
return SendResult.success();
} else {
String errMsg = response.getMessage();
log.error("短信发送失败, taskBizId={}, errCode={}, errMsg={}", task.getTaskBizId(), code, errMsg);
if ("isv.MOBILE_NUMBER_ILLEGAL".equals(code) || "isv.INVALID_PARAMETERS".equals(code)) {
return SendResult.failNonRetryable(code, errMsg);
}
return SendResult.failRetryable(code, errMsg);
}
} catch (ClientException e) {
log.error("短信发送异常, taskBizId={}", task.getTaskBizId(), e);
String errCode = e.getErrCode();
if ("isv.MOBILE_NUMBER_ILLEGAL".equals(errCode)) {
return SendResult.failNonRetryable(errCode, e.getMessage());
}
return SendResult.failRetryable(errCode, e.getMessage());
} catch (Exception e) {
log.error("短信发送异常, taskBizId={}", task.getTaskBizId(), e);
return SendResult.failRetryable("SEND_EXCEPTION", e.getMessage());
}
}
@Override
public String getSupportedChannelType() {
return "sms";
}
}
\ No newline at end of file
...@@ -2,49 +2,55 @@ package com.yd.notice.service.send; ...@@ -2,49 +2,55 @@ package com.yd.notice.service.send;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.yd.notice.service.model.ChannelConfig; import com.yd.notice.service.model.ChannelConfig;
import com.yd.notice.service.service.IChannelConfigService;
import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
import me.chanjar.weixin.cp.bean.article.NewArticle;
import com.yd.notice.service.model.NotificationTask; import com.yd.notice.service.model.NotificationTask;
import com.yd.notice.service.service.IChannelConfigService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
import me.chanjar.weixin.cp.bean.message.WxCpMessage; import me.chanjar.weixin.cp.bean.message.WxCpMessage;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Slf4j @Slf4j
@Service @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class WecomMessageSender { public class WecomMessageSender implements MessageSender {
private final IChannelConfigService channelConfigService; private final IChannelConfigService channelConfigService;
/** // 不可重试的错误码(永久失败)
* 发送文本消息(动态获取渠道配置) private static final List<String> NON_RETRYABLE_ERROR_CODES = Arrays.asList(
*/ "81013", // UserID 无效或不存在
public boolean sendTextMessage(NotificationTask task) { "60020", // 应用无权限发送给该用户
"60102", // UserID 不存在
"40003", // 不合法的 UserID
"40014" // 不合法的 access_token
);
@Override
public SendResult send(NotificationTask task) {
try { try {
// 1. 根据 channelBizId 查询渠道配置 // 1. 查询渠道配置
ChannelConfig config = channelConfigService.getByChannelBizId(task.getChannelBizId()); ChannelConfig config = channelConfigService.getByChannelBizId(task.getChannelBizId());
if (config == null || config.getStatus() != 1) { if (config == null || config.getStatus() != 1) {
log.error("渠道配置不存在或已禁用,channelBizId: {}", task.getChannelBizId()); return SendResult.failNonRetryable("CONFIG_NOT_EXIST", "渠道配置不存在或已禁用");
return false;
} }
if (!"wecom".equals(config.getChannelType())) { if (!"wecom".equals(config.getChannelType())) {
log.error("渠道类型不是企业微信,channelBizId: {}", task.getChannelBizId()); return SendResult.failNonRetryable("CHANNEL_TYPE_ERROR", "渠道类型不是企业微信");
return false;
} }
// 2. 解析 config_value(JSON格式,需解密) // 2. 解析配置(JSON格式,生产环境需解密)
String configValueJson = config.getConfigValue(); JSONObject wecomConfig = JSONObject.parseObject(config.getConfigValue());
// TODO: 若 config_value 是加密的,此处需要调用解密工具解密
JSONObject wecomConfig = JSONObject.parseObject(configValueJson);
String corpId = wecomConfig.getString("corpId"); String corpId = wecomConfig.getString("corpId");
Integer agentId = wecomConfig.getInteger("agentId"); Integer agentId = wecomConfig.getInteger("agentId");
String secret = wecomConfig.getString("secret"); String secret = wecomConfig.getString("secret");
// 3. 动态创建 WxCpService(每次新建,可增加缓存优化) // 3. 动态创建 WxCpService(可增加缓存优化)
WxCpDefaultConfigImpl wxConfig = new WxCpDefaultConfigImpl(); WxCpDefaultConfigImpl wxConfig = new WxCpDefaultConfigImpl();
wxConfig.setCorpId(corpId); wxConfig.setCorpId(corpId);
wxConfig.setAgentId(agentId); wxConfig.setAgentId(agentId);
...@@ -59,46 +65,26 @@ public class WecomMessageSender { ...@@ -59,46 +65,26 @@ public class WecomMessageSender {
.content(task.getContent()) .content(task.getContent())
.build(); .build();
wxCpService.getMessageService().send(message); wxCpService.getMessageService().send(message);
log.info("企业微信消息发送成功,taskBizId: {}, channelBizId: {}", task.getTaskBizId(), task.getChannelBizId());
return true;
log.info("企业微信消息发送成功, taskBizId={}, receiver={}", task.getTaskBizId(), task.getReceiver());
return SendResult.success();
} catch (WxErrorException e) {
String errCode = String.valueOf(e.getError().getErrorCode());
String errMsg = e.getMessage();
log.error("企业微信发送失败, taskBizId={}, errCode={}, errMsg={}", task.getTaskBizId(), errCode, errMsg);
if (NON_RETRYABLE_ERROR_CODES.contains(errCode)) {
return SendResult.failNonRetryable(errCode, errMsg);
}
return SendResult.failRetryable(errCode, errMsg);
} catch (Exception e) { } catch (Exception e) {
log.error("企业微信消息发送失败,taskBizId: {}, error: {}", task.getTaskBizId(), e.getMessage(), e); log.error("企业微信发送异常, taskBizId={}", task.getTaskBizId(), e);
return false; return SendResult.failRetryable("SEND_EXCEPTION", e.getMessage());
} }
} }
@Override
// /** public String getSupportedChannelType() {
// * 发送图文卡片消息(带跳转链接) return "wecom";
// */ }
// public boolean sendNewsMessage(NotificationTask task, String url, String description) {
// try {
// // 使用 builder() 构建 NewArticle 对象
// NewArticle article = NewArticle.builder()
// .title(task.getTitle()) // 消息标题
// .description(description) // 消息描述
// .url(url) // 点击后跳转的链接
// // .picUrl("https://your-domain.com/cover.jpg") // 可选:封面图片
// // .btnText("查看详情") // 可选:按钮文字
// .build();
//
// // 构建 WxCpMessage 消息体
// WxCpMessage message = WxCpMessage.NEWS()
// .agentId(wxCpService.getWxCpConfigStorage().getAgentId())
// .toUser(task.getReceiver())
// .addArticle(article) // 使用NewArticle对象
// .build();
//
// // 发送消息
// wxCpService.getMessageService().send(message);
// log.info("企业微信图文消息发送成功,taskBizId: {}", task.getTaskBizId());
// return true;
//
// } catch (Exception e) {
// log.error("企业微信图文消息发送失败,taskBizId: {}", task.getTaskBizId(), e);
// return false;
// }
// }
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment