This commit is contained in:
caozehui
2026-02-10 15:27:28 +08:00
parent 3f65a55c15
commit 1219b30f43
52 changed files with 1408 additions and 22 deletions

View File

@@ -6,6 +6,7 @@ import org.springframework.core.Ordered;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.stereotype.Component;
/**
* 自定义的 URL 的安全配置

View File

@@ -38,7 +38,7 @@ public class MsgpushSecurityAutoConfiguration {
private SecurityProperties securityProperties;
/**
* 认证失败处理类 Bean
* 身份认证失败处理类 Bean
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {

View File

@@ -8,6 +8,7 @@ import com.google.common.collect.Multimap;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.DispatcherType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.context.ApplicationContext;

View File

@@ -15,7 +15,7 @@ import org.springframework.util.Assert;
public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
/**
* 使用 TransmittableThreadLocal 作为上下文
* 使用 TransmittableThreadLocal 实现线程之间上下文的传递。
*/
private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>();

View File

@@ -1,5 +1,4 @@
com.njcn.msgpush.framework.security.config.MsgpushSecurityRpcAutoConfiguration
com.njcn.msgpush.framework.security.config.MsgpushSecurityAutoConfiguration
com.njcn.msgpush.framework.security.config.MsgpushWebSecurityConfigurerAdapter
com.njcn.msgpush.framework.operatelog.config.MsgpushOperateLogConfiguration
com.njcn.msgpush.framework.operatelog.config.MsgpushOperateLogRpcAutoConfiguration

View File

@@ -0,0 +1,16 @@
package com.njcn.msgpush.module.push.annoation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author caozehui
* @data 2026-02-10
* @description 接口幂等性检查注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckIdmpotent {
}

View File

@@ -105,6 +105,20 @@
<artifactId>hutool-extra</artifactId> <!-- 邮件 -->
</dependency>
<!-- 阿里云 Direct Mail (邮箱服务) -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dm20151123</artifactId>
<version>1.9.3</version>
</dependency>
<!-- 阿里云短信服务 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>4.2.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -2,9 +2,11 @@ package com.njcn.msgpush.module.push;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* 项目的启动类
*
* @author hongawen
*/
@SpringBootApplication

View File

@@ -0,0 +1,13 @@
package com.njcn.msgpush.module.push.checker;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
/**
* @author caozehui
* @data 2026-02-27
* @description 检查器接口
*/
public interface IChecker {
boolean check(MessageRecordSendReqVO reqVO);
}

View File

@@ -0,0 +1,44 @@
package com.njcn.msgpush.module.push.checker;
import com.njcn.msgpush.module.push.checker.impl.BlacklistChecker;
import com.njcn.msgpush.module.push.checker.impl.IdempotencyChecker;
import com.njcn.msgpush.module.push.checker.impl.QuotaChecker;
import com.njcn.msgpush.module.push.checker.impl.RateLimitChecker;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
import java.util.ArrayList;
import java.util.List;
/**
* @author caozehui
* @data 2026-02-28
* @description 检查链
*/
public class MsgPushGuardChain {
private final List<IChecker> checkers;
public MsgPushGuardChain() {
this.checkers = new ArrayList<>();
this.checkers.add(new IdempotencyChecker());
this.checkers.add(new BlacklistChecker());
this.checkers.add(new QuotaChecker());
this.checkers.add(new RateLimitChecker());
}
public boolean checkAll(MessageRecordSendReqVO reqVO) {
for (IChecker checker : checkers) {
boolean result = checker.check(reqVO);
if (!result) {
// 任何一层检查失败,立即返回拒绝
logRejection(reqVO);
return result;
}
}
return true;
}
private void logRejection(MessageRecordSendReqVO reqVO) {
// 记录拒绝日志,用于监控和分析
System.out.printf("消息请求被拒绝: receiver=%s, messageId=%s, reason=%s%n", reqVO.getReceiver(), reqVO.getMessageId());
}
}

View File

@@ -0,0 +1,16 @@
package com.njcn.msgpush.module.push.checker.impl;
import com.njcn.msgpush.module.push.checker.IChecker;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
/**
* @author caozehui
* @data 2026-02-27
* @description 黑名单检查器
*/
public class BlacklistChecker implements IChecker {
@Override
public boolean check(MessageRecordSendReqVO reqVO) {
return true;
}
}

View File

@@ -0,0 +1,16 @@
package com.njcn.msgpush.module.push.checker.impl;
import com.njcn.msgpush.module.push.checker.IChecker;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
/**
* @author caozehui
* @data 2026-02-27
* @description 接口幂等性检查器
*/
public class IdempotencyChecker implements IChecker {
@Override
public boolean check(MessageRecordSendReqVO reqVO) {
return true;
}
}

View File

@@ -0,0 +1,16 @@
package com.njcn.msgpush.module.push.checker.impl;
import com.njcn.msgpush.module.push.checker.IChecker;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
/**
* @author caozehui
* @data 2026-02-27
* @description 系统配额检查器
*/
public class QuotaChecker implements IChecker {
@Override
public boolean check(MessageRecordSendReqVO reqVO) {
return true;
}
}

View File

@@ -0,0 +1,17 @@
package com.njcn.msgpush.module.push.checker.impl;
import com.njcn.msgpush.module.push.checker.IChecker;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
/**
* @author caozehui
* @data 2026-02-27
* @description 接收者频率检查器
*/
public class RateLimitChecker implements IChecker {
@Override
public boolean check(MessageRecordSendReqVO reqVO) {
return true;
}
}

View File

@@ -0,0 +1,8 @@
package com.njcn.msgpush.module.push.client.channel.appPush;
/**
* @author caozehui
* @data 2026-02-11
*/
public interface AppPushClient {
}

View File

@@ -0,0 +1,8 @@
package com.njcn.msgpush.module.push.client.channel.appPush.factory;
/**
* @author caozehui
* @data 2026-02-11
*/
public class AppPushFactory {
}

View File

@@ -0,0 +1,15 @@
package com.njcn.msgpush.module.push.client.channel.mail;
import java.util.List;
import java.util.Map;
/**
* @author caozehui
* @data 2026-02-11
*/
public interface MailClient {
void sendMail(String accountName, Integer addressType, Boolean replyToAddress, String toAddress, String subject, String htmlBody);
void queryMailAddressByParam();
}

View File

@@ -0,0 +1,27 @@
package com.njcn.msgpush.module.push.client.channel.mail.factory;
import cn.hutool.core.util.StrUtil;
import com.njcn.msgpush.module.push.client.channel.mail.MailClient;
import com.njcn.msgpush.module.push.client.channel.mail.impl.AliYunMailClient;
import com.njcn.msgpush.module.push.client.constant.ClientConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author caozehui
* @data 2026-02-11
*/
@Component
public class MailFactory {
@Autowired
private AliYunMailClient aliYunMailClient;
public MailClient getClient(String mailProviderTypeName) throws RuntimeException {
if (StrUtil.equals(ClientConstant.ALI_YUN, mailProviderTypeName)) {
return aliYunMailClient;
} else {
throw new RuntimeException("暂时不提供" + mailProviderTypeName + "邮件服务提供商");
}
}
}

View File

@@ -0,0 +1,86 @@
package com.njcn.msgpush.module.push.client.channel.mail.impl;
import com.alibaba.fastjson.JSON;
import com.aliyun.dm20151123.Client;
import com.aliyun.dm20151123.models.QueryMailAddressByParamRequest;
import com.aliyun.dm20151123.models.QueryMailAddressByParamResponse;
import com.aliyun.dm20151123.models.SingleSendMailRequest;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.njcn.msgpush.module.push.client.channel.mail.MailClient;
import com.njcn.msgpush.module.push.client.setting.mail.AliYunMailSetting;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
/**
* @author caozehui
* @data 2026-02-11
*/
@Slf4j
@Service
public class AliYunMailClient implements MailClient {
private static final String ACCOUNT_NAME = "accountName";
private static final Integer ADDRESS_TYPE = 0;
private static final String REPLY_TO_ADDRESS = "replyToAddress";
private static final String HTML_BODY = "htmlBody";
private static final String TEXT_BODY = "textBody";
private static final String TO_ADDRESS = "toAddress";
private static final String SUBJECT = "subject";
private static final String CLICK_TRACE = "1";
@Autowired
private AliYunMailSetting aliYunMailSetting;
private Client client;
@PostConstruct
public void init() {
Config config = new Config()
.setAccessKeyId(aliYunMailSetting.getAccessKeyId())
.setAccessKeySecret(aliYunMailSetting.getAccessKeySecret())
.setRegionId(aliYunMailSetting.getRegionId())
.setEndpoint(aliYunMailSetting.getEndpoint());
try {
this.client = new Client(config);
} catch (Exception e) {
log.error("阿里云-邮件服务初始化失败,请检查配置信息");
throw new RuntimeException(e);
}
}
@Override
public void sendMail(String accountName, Integer addressType, Boolean replyToAddress, String toAddress, String subject, String htmlBody) {
RuntimeOptions runtimeOptions = new RuntimeOptions();
// 设置自动重试默认是不开启的。重试次数默认是3次
runtimeOptions.autoretry = true;
SingleSendMailRequest request = new SingleSendMailRequest()
.setAccountName(accountName)
.setAddressType(addressType)
.setReplyToAddress(replyToAddress)
.setToAddress(toAddress)
.setSubject(subject)
.setHtmlBody(htmlBody);
try {
client.singleSendMailWithOptions(request, runtimeOptions);
} catch (Exception e) {
throw new RuntimeException("阿里云-邮件服务发送失败");
}
}
@Override
public void queryMailAddressByParam() {
QueryMailAddressByParamRequest queryMailAddressByParamRequest = null;
try {
queryMailAddressByParamRequest = QueryMailAddressByParamRequest.build(new HashMap<>());
QueryMailAddressByParamResponse response = client.queryMailAddressByParam(queryMailAddressByParamRequest);
System.out.println(JSON.toJSON(response));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,144 @@
package com.njcn.msgpush.module.push.client.channel.sms.Impl;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.*;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.njcn.msgpush.module.push.client.channel.sms.SmsClient;
import com.njcn.msgpush.module.push.client.setting.sms.AliYunSmsSetting;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.aliyun.teautil.Common.toJSONString;
/**
* @author caozehui
* @data 2026-02-11
* @description 阿里云短信服务实现
*/
@Slf4j
@Service
public class AliYunSmsClient implements SmsClient {
public static final String SIGN_NAME = "signName";
public static final String TEMPLATE_CODE = "templateCode";
public static final String TEMPLATE_PARAM = "templateParam";
public static final String OK = "OK";
@Autowired
private AliYunSmsSetting aliYunSmsSetting;
private Client client;
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
5,
5,
1000,
TimeUnit.MILLISECONDS,
new java.util.concurrent.ArrayBlockingQueue<>(1000),
r -> {
Thread thread = new Thread(r);
thread.setName("AliYunSmsClient-Pool-" + thread.getId());
thread.setDaemon(false);
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
@PostConstruct
public void init() {
Config config = new Config()
.setAccessKeyId(aliYunSmsSetting.getAccessKeyId())
.setAccessKeySecret(aliYunSmsSetting.getAccessKeySecret())
.setRegionId(aliYunSmsSetting.getRegionId())
.setEndpoint(aliYunSmsSetting.getEndpoint());
try {
this.client = new Client(config);
} catch (Exception e) {
log.error("阿里云-短信服务初始化失败,请检查配置信息");
throw new RuntimeException(e);
}
}
@Override
public boolean sendSms(Map<String, Object> params, String phoneNumber) throws Exception {
Future<Boolean> future = THREAD_POOL_EXECUTOR.submit(() -> {
// todo 修改消息的状态为 sending
RuntimeOptions runtimeOptions = new RuntimeOptions();
// 设置自动重试默认是不开启的。重试次数默认是3次
runtimeOptions.autoretry = true;
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phoneNumber)
.setSignName(params.get(SIGN_NAME).toString())
.setTemplateCode(params.get(TEMPLATE_CODE).toString())
.setTemplateParam(params.get(TEMPLATE_PARAM).toString());
try {
SendSmsResponse response = this.client.sendSmsWithOptions(request, runtimeOptions);
System.out.println(toJSONString(response));
if (OK.equals(response.body.code)) {
return true;
} else {
return false;
}
} catch (Exception e) {
log.error("阿里云-短信服务发送失败");
throw new Exception(e);
}
});
Boolean b = future.get(3, TimeUnit.SECONDS);
if (b) {
// todo 修改消息的状态为 success
} else {
// todo 修改消息的状态为 failed
}
return b;
}
@Override
public boolean sendBatchSms(Map<String, Object> params, List<String> phoneNumbers) {
RuntimeOptions runtimeOptions = new RuntimeOptions();
// 设置自动重试默认是不开启的。重试次数默认是3次
runtimeOptions.autoretry = true;
SendBatchSmsRequest request = new SendBatchSmsRequest()
.setPhoneNumberJson(toJSONString(phoneNumbers))
.setSignNameJson(toJSONString(params.get(SIGN_NAME)))
.setTemplateCode(params.get(TEMPLATE_CODE).toString())
.setTemplateParamJson(toJSONString(params.get(TEMPLATE_PARAM)));
try {
SendBatchSmsResponse response = this.client.sendBatchSmsWithOptions(request, runtimeOptions);
System.out.println(toJSONString(response));
if (OK.equals(response.body.code)) {
return true;
} else {
return false;
}
} catch (Exception e) {
log.error("阿里云-短信服务发送失败");
throw new RuntimeException(e);
}
}
@Override
public void querySmsTemplateList() {
QuerySmsTemplateListRequest request = new QuerySmsTemplateListRequest();
request.setPageIndex(1);
request.setPageSize(10);
try {
QuerySmsTemplateListResponse querySmsTemplateListResponse = this.client.querySmsTemplateList(request);
System.out.println(toJSONString(querySmsTemplateListResponse));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,89 @@
package com.njcn.msgpush.module.push.client.channel.sms.Impl;
import cn.hutool.core.util.StrUtil;
import com.njcn.msgpush.module.push.client.channel.sms.SmsClient;
import com.njcn.msgpush.module.push.client.setting.sms.TelecomSmsSetting;
import com.njcn.msgpush.module.push.util.RestTemplateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author caozehui
* @data 2026-02-11
* @description 电信e企云短信服务实现
*/
@Slf4j
@Service
public class TelecomSmsClient implements SmsClient {
/**
* 短信接口地址
*/
private static final String API_URL = "https://sms.ymeeting.cn/smsv2";
/**
* 虚拟接入码
*/
private static final String ACCESS_CODE = "106905631";
/**
* 短信接口内容类型
*/
private static final String CONTENT_TYPE = "application/json;charset=utf-8";
// public static final String ACCOUNT = "account";
// public static final String PASSWORD = "password";
public static final String CONTENT = "content";
@Autowired
private TelecomSmsSetting telecomSmsSetting;
@Autowired
private RestTemplateUtil restTemplateUtil;
@Override
public boolean sendSms(Map<String, Object> params, String phoneNumber) throws Exception {
return this.sendBatchSms(params, List.of(phoneNumber));
}
@Override
public boolean sendBatchSms(Map<String, Object> params, List<String> phoneNumbers) {
// 构建请求参数
Map<String, Object> request = new HashMap<>();
request.put("action", "send");
request.put("account", telecomSmsSetting.getAccount());
request.put("password", telecomSmsSetting.getPassword());
request.put("mobile", StrUtil.join(StrUtil.COMMA, phoneNumbers));
request.put("content", params.get(CONTENT).toString());
request.put("extno", ACCESS_CODE);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", CONTENT_TYPE);
// 发送请求
ResponseEntity<String> response = restTemplateUtil.post(
API_URL,
request,
headers,
String.class
);
String body = response.getBody();
if (body.contains("\"status\": 0")) {
return true;
} else {
return false;
}
}
@Override
public void querySmsTemplateList() {
}
}

View File

@@ -0,0 +1,31 @@
package com.njcn.msgpush.module.push.client.channel.sms;
import java.util.List;
import java.util.Map;
/**
* @author caozehui
* @data 2026-02-10
*/
public interface SmsClient {
/**
* 向单个手机号发送短信
*
* @param phoneNumber 手机号
* @param params 参数
* @return 发送结果
*/
boolean sendSms(Map<String, Object> params, String phoneNumber) throws Exception;
/**
* 向多个手机号发送短信
*
* @param phoneNumbers 手机号集合
* @param params 参数
* @return 发送结果
*/
boolean sendBatchSms(Map<String, Object> params, List<String> phoneNumbers);
void querySmsTemplateList();
}

View File

@@ -0,0 +1,33 @@
package com.njcn.msgpush.module.push.client.channel.sms.factory;
import cn.hutool.core.util.StrUtil;
import com.njcn.msgpush.module.push.client.channel.sms.Impl.AliYunSmsClient;
import com.njcn.msgpush.module.push.client.channel.sms.Impl.TelecomSmsClient;
import com.njcn.msgpush.module.push.client.channel.sms.SmsClient;
import com.njcn.msgpush.module.push.client.constant.ClientConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author caozehui
* @data 2026-02-11
*/
@Component
public class SmsFactory {
@Autowired
private AliYunSmsClient aliYunSmsClient;
@Autowired
private TelecomSmsClient telecomSmsClient;
public SmsClient getClient(String smsProviderTypeName) throws RuntimeException {
if (StrUtil.equals(ClientConstant.ALI_YUN, smsProviderTypeName)) {
return aliYunSmsClient;
} else if (StrUtil.equals(ClientConstant.TELECOM, smsProviderTypeName)) {
return telecomSmsClient;
} else {
throw new RuntimeException("暂时不提供" + smsProviderTypeName + "短信服务提供商");
}
}
}

View File

@@ -0,0 +1,11 @@
package com.njcn.msgpush.module.push.client.constant;
/**
* @author caozehui
* @data 2026-02-10
*/
public interface ClientConstant {
String ALI_YUN = "阿里云";
String TELECOM = "电信";
}

View File

@@ -0,0 +1,9 @@
package com.njcn.msgpush.module.push.client.setting;
/**
* @author caozehui
* @data 2026-02-09
* @description 各个推送渠道通用的配置
*/
public abstract class BaseChannelSetting {
}

View File

@@ -0,0 +1,18 @@
package com.njcn.msgpush.module.push.client.setting.appPush;
import com.njcn.msgpush.module.push.client.setting.BaseChannelSetting;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author caozehui
* @data 2026-02-09
* @description App推送配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class AppPushSetting extends BaseChannelSetting {
private String appKey; //示例
private String secret; //示例
private String key; //示例
}

View File

@@ -0,0 +1,20 @@
package com.njcn.msgpush.module.push.client.setting.appPush;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* @author caozehui
* @data 2026-02-09
* @description UniPush应用推送配置
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class UniPushAppPushSetting extends AppPushSetting {
private String accessKeyId;
private String accessKeySecret;
}

View File

@@ -0,0 +1,22 @@
package com.njcn.msgpush.module.push.client.setting.mail;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author caozehui
* @data 2026-02-09
* @description 阿里云邮件配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Component
@ConfigurationProperties(prefix = "aliyun.mail")
public class AliYunMailSetting extends MailSetting {
private String accessKeyId;
private String accessKeySecret;
private String regionId;
private String endpoint;
}

View File

@@ -0,0 +1,16 @@
package com.njcn.msgpush.module.push.client.setting.mail;
import com.njcn.msgpush.module.push.client.setting.BaseChannelSetting;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author caozehui
* @data 2026-02-09
* @description 邮箱配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class MailSetting extends BaseChannelSetting {
}

View File

@@ -0,0 +1,22 @@
package com.njcn.msgpush.module.push.client.setting.sms;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author caozehui
* @data 2026-02-09
* @description 阿里云短信应用配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Component
@ConfigurationProperties(prefix = "aliyun.sms")
public class AliYunSmsSetting extends SmsSetting {
private String accessKeyId;
private String accessKeySecret;
private String regionId;
private String endpoint;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.msgpush.module.push.client.setting.sms;
import com.njcn.msgpush.module.push.client.setting.BaseChannelSetting;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author caozehui
* @data 2026-02-09
* @description 短信配置抽象类
*/
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class SmsSetting extends BaseChannelSetting {
}

View File

@@ -0,0 +1,20 @@
package com.njcn.msgpush.module.push.client.setting.sms;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author caozehui
* @data 2026-02-10
* @description 电信e企云短信服务应用配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Component
@ConfigurationProperties(prefix = "telecom.sms")
public class TelecomSmsSetting extends SmsSetting {
private String account;
private String password;
}

View File

@@ -0,0 +1,22 @@
package com.njcn.msgpush.module.push.controller.admin.channel;
import com.njcn.msgpush.module.push.service.channel.ChannelProviderConfigService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "管理后台 - 渠道服务商")
@Slf4j
@Validated
@RestController
@RequestMapping("/push/channel")
public class ChannelProviderConfigController {
@Autowired
private ChannelProviderConfigService channelProviderConfigService;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.msgpush.module.push.controller.admin.channel.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "管理后台 - 渠道服务商配置 Request VO")
public class ChannelProviderConfigReqVO {
@Schema(description = "渠道类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sms/email/app_push")
private String channel;
@Schema(description = "服务商名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "中国电信/阿里云/UniPush")
private String providerName;
@Schema(description = "服务商类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "telecom/cmcc/aliyun/twilio/unipush")
private String providerType;
@Schema(description = "API地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://api.example.com")
private String apiUrl;
@Schema(description = "AppKey", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
private String appKey;
@Schema(description = "AppSecret", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
private String appSecret;
@Schema(description = "额外配置JSON格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private String extraConfig;
@Schema(description = "优先级(数字越小优先级越高)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer priority;
}

View File

@@ -0,0 +1,35 @@
package com.njcn.msgpush.module.push.controller.admin.message;
import com.njcn.msgpush.framework.common.pojo.CommonResult;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
import com.njcn.msgpush.module.push.service.message.MessageRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.security.PermitAll;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "管理后台 - 消息")
@Slf4j
@Validated
@RestController
@RequestMapping("/push/message")
public class MessageRecordController {
@Autowired
private MessageRecordService messageRecordService;
@PostMapping("send")
@PermitAll
@Operation(summary = "使用账号密码登录")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<Boolean> send(MessageRecordSendReqVO messageRecordSendReqVO) {
Boolean result = messageRecordService.send(messageRecordSendReqVO);
return CommonResult.success(result);
}
}

View File

@@ -0,0 +1,42 @@
package com.njcn.msgpush.module.push.controller.admin.message.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "管理后台 - 消息记录发送 Request VO")
public class MessageRecordSendReqVO {
@Schema(description = "消息唯一ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
private String messageId;
@Schema(description = "应用名称/来源系统标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "NPQS-9500")
private String appName;
@Schema(description = "渠道类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sms/email/app_push")
private String channel;
@Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "verify_code/order_notify/marketing/system_notify")
private String messageType;
@Schema(description = "接收者", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300")
private String receiver;
@Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED)
private String title;
@Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED)
private String templateCode;
@Schema(description = "模板参数")
private String templateParams;
@Schema(description = "服务商类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "telecom/cmcc/aliyun/twilio")
private String providerType;
@Schema(description = "第三方消息ID")
private String thirdPartyId;
}

View File

@@ -0,0 +1,84 @@
package com.njcn.msgpush.module.push.dal.dataobject.channel;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.msgpush.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* @author caozehui
* @data 2026-02-06
* @description 渠道服务商配置表对应的数据对象
*/
@Data
@TableName("push_channel_provider_config")
@EqualsAndHashCode(callSuper = true)
public class ChannelProviderConfigDO extends BaseDO {
/**
* 主键ID
*/
private Long id;
/**
* 渠道类型sms/email/app_push
*/
private String channel;
/**
* 服务商名称:中国电信/阿里云/UniPush
*/
private String providerName;
/**
* 服务商类型telecom/cmcc/aliyun/twilio/unipush
*/
private String providerType;
/**
* API地址
*/
private String apiUrl;
/**
* AppKey
*/
private String appKey;
/**
* AppSecret
*/
private String appSecret;
/**
* 额外配置JSON格式
*/
private String extraConfig;
/**
* 优先级(数字越小优先级越高)
*/
private Integer priority;
/**
* 是否启用0-禁用 1-启用(手动控制)
*/
private Integer enabled;
/**
* 健康状态0-异常 1-正常(自动检测)
*/
private Integer healthStatus;
/**
* 连续失败次数(自动统计)
*/
private Integer failureCount;
/**
* 最后失败时间(自动记录)
*/
private LocalDateTime lastFailureTime;
}

View File

@@ -0,0 +1,123 @@
package com.njcn.msgpush.module.push.dal.dataobject.message;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.msgpush.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* @author caozehui
* @data 2026-02-06
* @description 消息记录表对应的数据对象
*/
@Data
@TableName("push_message_record")
@EqualsAndHashCode(callSuper = true)
public class MessageRecordDO extends BaseDO {
/**
* 主键ID
*/
private Long id;
/**
* 消息唯一ID
*/
private String messageId;
/**
* 应用名称/来源系统标识
*/
private String appName;
/**
* 渠道类型sms/email/app_push
*/
private String channel;
/**
* 消息类型verify_code/order_notify/marketing/system_notify
*/
private String messageType;
/**
* 接收者
*/
private String receiver;
/**
* 标题
*/
private String title;
/**
* 消息内容
*/
private String content;
/**
* 模板编码
*/
private String templateCode;
/**
* 模板参数
*/
private String templateParams;
/**
* 状态pending/sending/success/failed/final_failed/blacklisted/quota_exceeded/rate_limited/abandoned
*/
private String status;
/**
* 发送时间
*/
private LocalDateTime sendTime;
/**
* 发送耗时(毫秒)
*/
private Integer costTime;
/**
* 已重试次数
*/
private Integer retryCount;
/**
* 最后重试时间
*/
private LocalDateTime lastRetryTime;
/**
* 下次重试时间
*/
private LocalDateTime nextRetryTime;
/**
* 服务商类型telecom/cmcc/aliyun/twilio
*/
private String providerType;
/**
* 第三方消息ID
*/
private String thirdPartyId;
/**
* 统一错误码
*/
private String errorCode;
/**
* 错误信息
*/
private String errorMsg;
/**
* 过期时间
*/
private LocalDateTime expireTime;
}

View File

@@ -0,0 +1,9 @@
package com.njcn.msgpush.module.push.dal.mysql.channel;
import com.njcn.msgpush.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.msgpush.module.push.dal.dataobject.channel.ChannelProviderConfigDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ChannelProviderConfigMapper extends BaseMapperX<ChannelProviderConfigDO> {
}

View File

@@ -0,0 +1,9 @@
package com.njcn.msgpush.module.push.dal.mysql.message;
import com.njcn.msgpush.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MessageRecordMapper extends BaseMapperX<MessageRecordDO> {
}

View File

@@ -0,0 +1,4 @@
package com.njcn.msgpush.module.push.service.channel;
public interface ChannelProviderConfigService {
}

View File

@@ -0,0 +1,7 @@
package com.njcn.msgpush.module.push.service.channel;
import org.springframework.stereotype.Service;
@Service
public class ChannelProviderConfigServiceImpl implements ChannelProviderConfigService {
}

View File

@@ -0,0 +1,14 @@
package com.njcn.msgpush.module.push.service.message;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
public interface MessageRecordService {
/**
* 发送消息包括email、sms、app_push
*
* @param messageRecordSendReqVO
* @return 发送是否成功的结果
*/
Boolean send(MessageRecordSendReqVO messageRecordSendReqVO);
}

View File

@@ -0,0 +1,13 @@
package com.njcn.msgpush.module.push.service.message;
import com.njcn.msgpush.module.push.controller.admin.message.vo.MessageRecordSendReqVO;
import org.springframework.stereotype.Service;
@Service
public class MessageRecordServiceImpl implements MessageRecordService{
@Override
public Boolean send(MessageRecordSendReqVO messageRecordSendReqVO) {
return null;
}
}

View File

@@ -0,0 +1,121 @@
package com.njcn.msgpush.module.push.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* @author caozehui
* @data 2026-02-10
* @description restTemplate工具类
*/
@Slf4j
@Component
public class RestTemplateUtil {
private final RestTemplate restTemplate;
public RestTemplateUtil() {
this.restTemplate = new RestTemplate();
}
public <T> ResponseEntity<T> get(String url, Class<T> responseType) {
return get(url, null, responseType);
}
public <T> ResponseEntity<T> get(String url, HttpHeaders headers, Class<T> responseType) {
try {
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
log.info("发送GET请求到: {}", url);
ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.GET, requestEntity, responseType);
log.info("GET请求响应状态: {}", response.getStatusCode());
return response;
} catch (Exception e) {
log.error("GET请求异常: {}", e.getMessage(), e);
throw e;
}
}
public <T> ResponseEntity<T> post(String url, Object requestBody, Class<T> responseType) {
return post(url, requestBody, null, responseType);
}
public <T> ResponseEntity<T> post(String url, Object requestBody, HttpHeaders headers, Class<T> responseType) {
try {
if (headers == null) {
headers = new HttpHeaders();
}
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Object> request = new HttpEntity<>(requestBody, headers);
log.info("发送POST请求到: {}", url);
ResponseEntity<T> response = restTemplate.postForEntity(url, request, responseType);
log.info("POST请求响应状态: {}", response.getStatusCode());
return response;
} catch (Exception e) {
log.error("POST请求异常: {}", e.getMessage(), e);
throw e;
}
}
public <T> ResponseEntity<T> postForm(String url, Map<String, String> formData, Class<T> responseType) {
return postForm(url, formData, null, responseType);
}
public <T> ResponseEntity<T> postForm(String url, Map<String, String> formData, HttpHeaders headers, Class<T> responseType) {
try {
if (headers == null) {
headers = new HttpHeaders();
}
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<Map<String, String>> request = new HttpEntity<>(formData, headers);
log.info("发送POST表单请求到: {}", url);
ResponseEntity<T> response = restTemplate.postForEntity(url, request, responseType);
log.info("POST表单请求响应状态: {}", response.getStatusCode());
return response;
} catch (Exception e) {
log.error("POST表单请求异常: {}", e.getMessage(), e);
throw e;
}
}
public <T> T getForObject(String url, Class<T> responseType) {
try {
log.info("发送GET请求获取对象到: {}", url);
T result = restTemplate.getForObject(url, responseType);
log.info("GET请求成功获取对象");
return result;
} catch (Exception e) {
log.error("GET请求获取对象异常: {}", e.getMessage(), e);
throw e;
}
}
public <T> T postForObject(String url, Object requestBody, Class<T> responseType) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Object> request = new HttpEntity<>(requestBody, headers);
log.info("发送POST请求获取对象到: {}", url);
T result = restTemplate.postForObject(url, request, responseType);
log.info("POST请求成功获取对象");
return result;
} catch (Exception e) {
log.error("POST请求获取对象异常: {}", e.getMessage(), e);
throw e;
}
}
public String getString(String url) {
return getForObject(url, String.class);
}
public String postString(String url, Object requestBody) {
return postForObject(url, requestBody, String.class);
}
}

View File

@@ -7,13 +7,13 @@ spring:
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
namespace: msgCenter # 命名空间。这里使用 dev 开发环境
group: DEV # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
namespace: msgCenter # 命名空间。这里使用 dev 开发环境
group: DEV # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
--- #################### 数据库相关配置 ####################
spring:

View File

@@ -1,23 +1,30 @@
--- #################### 注册中心 + 配置中心相关配置 ####################
#spring:
# cloud:
# nacos:
# server-addr: 192.168.1.103:18848 # Nacos 服务器地址
# username: # Nacos 账号
# password: # Nacos 密码
# discovery: # 【配置中心】配置项
# namespace: msgCenter # 命名空间。这里使用 dev 开发环境
# group: DEV # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
# metadata:
# version: 1.0.0 # 服务实例的版本号,可用于灰度发布
# config: # 【注册中心】配置项
# namespace: msgCenter # 命名空间。这里使用 dev 开发环境
# group: DEV # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
spring:
cloud:
nacos:
server-addr: 192.168.1.103:18848 # Nacos 服务器地址
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
discovery:
enabled: false
config:
enabled: false
--- #################### 数据库相关配置 ####################
spring:
# 数据源配置项
autoconfigure:
exclude:

View File

@@ -106,3 +106,20 @@ msgpush:
debug: false
aliyun:
sms:
access-key-id: LTAI4FxsR76x2dq3w9c5puUe
access-key-secret: GxkTR8fsrvHtixTlD9UPmOGli35tZs
regionId: cn-hangzhou
endpoint: dysmsapi.aliyuncs.com
mail:
access-key-id: LTAI4FxsR76x2dq3w9c5puUe
access-key-secret: GxkTR8fsrvHtixTlD9UPmOGli35tZs
regionId: cn-hangzhou
endpoint: dm.aliyuncs.com
telecom:
sms:
account: 925631
password: AMW2pOVrdky

View File

@@ -0,0 +1,18 @@
package com.njcn.msgpush.module.push;
import com.njcn.msgpush.module.push.client.channel.mail.impl.AliYunMailClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class AliYunMailClientTest {
@Autowired
private AliYunMailClient aliYunMailClient;
@Test
public void testQueryMailAddressByParam(){
aliYunMailClient.queryMailAddressByParam();
}
}

View File

@@ -0,0 +1,49 @@
package com.njcn.msgpush.module.push.sms;
import com.njcn.msgpush.module.push.client.channel.sms.Impl.AliYunSmsClient;
import com.njcn.msgpush.module.push.client.channel.sms.SmsClient;
import com.njcn.msgpush.module.push.client.channel.sms.factory.SmsFactory;
import com.njcn.msgpush.module.push.client.constant.ClientConstant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author caozehui
* @data 2026-02-06
*/
@SpringBootTest
public class AliYumSmsClientTest {
@Autowired
private SmsFactory smsFactory;
@Test
public void testSendSms() throws Exception {
Map<String, Object> params = new HashMap<>();
params.put(AliYunSmsClient.SIGN_NAME, "灿能云");
params.put(AliYunSmsClient.TEMPLATE_CODE, "SMS_481710295");
params.put(AliYunSmsClient.TEMPLATE_PARAM, "{\"code\":\"123456\"}");
boolean b = smsFactory.getClient("阿里云").sendSms(params, "18839431215");
System.out.println(System.currentTimeMillis() + " " + b);
}
@Test
public void testSendBatchSms() {
Map<String, Object> params = new HashMap<>();
params.put(AliYunSmsClient.SIGN_NAME, List.of("灿能云"));
params.put(AliYunSmsClient.TEMPLATE_CODE, "SMS_481710295");
params.put(AliYunSmsClient.TEMPLATE_PARAM, List.of("{\"code\":\"123456\"}"));
smsFactory.getClient("阿里云").sendBatchSms(params, List.of("18839431215"));
}
@Test
public void testQuerySmsTemplateListRequest() {
SmsClient client = smsFactory.getClient(ClientConstant.ALI_YUN);
client.querySmsTemplateList();
}
}

View File

@@ -0,0 +1,33 @@
package com.njcn.msgpush.module.push.sms;
import com.njcn.msgpush.module.push.client.channel.sms.Impl.TelecomSmsClient;
import com.njcn.msgpush.module.push.client.channel.sms.factory.SmsFactory;
import com.njcn.msgpush.module.push.client.constant.ClientConstant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* @author caozehui
* @data 2026-02-10
*/
@SpringBootTest
public class TelecomSmsClientTest {
@Autowired
private SmsFactory smsFactory;
@Test
public void testSendSms() throws Exception {
Map<String, Object> params = new HashMap<>();
// params.put(TelecomSmsClient.ACCOUNT, "925631");
// params.put(TelecomSmsClient.PASSWORD, "AMW2pOVrdky");
params.put(TelecomSmsClient.CONTENT, "【南京灿能电力】这是JUnit群发短信测试请忽略。时间" + LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
smsFactory.getClient(ClientConstant.TELECOM).sendSms(params, "18839431215");
}
}

View File

@@ -9,9 +9,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @author hongawen
*/
@SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${msgpush.info.base-package}
@SpringBootApplication(scanBasePackages = {"${msgpush.info.base-package}.server", "${msgpush.info.base-package}.module"},
excludeName = {
})
@SpringBootApplication(scanBasePackages = {"${msgpush.info.base-package}.server", "${msgpush.info.base-package}.module"}, excludeName = {})
public class MsgpushServerApplication {
public static void main(String[] args) {