mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-10-28 16:58:43 +08:00 
			
		
		
		
	重构短信功能
This commit is contained in:
		| @@ -884,75 +884,4 @@ INSERT INTO `sys_user_role` VALUES (5, 100, 1, '', NULL, '', NULL, b'0'); | ||||
| INSERT INTO `sys_user_role` VALUES (6, 100, 2, '', NULL, '', NULL, b'0'); | ||||
| COMMIT; | ||||
|  | ||||
|  | ||||
| -- ---------------------------- | ||||
| -- Table structure for sms_channel | ||||
| -- ---------------------------- | ||||
| DROP TABLE IF EXISTS `sms_channel`; | ||||
| CREATE TABLE `sms_channel` ( | ||||
|     `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增编号', | ||||
|     `code` varchar(50) not null COMMENT '编码(来自枚举类 阿里、华为、七牛等)', | ||||
|     `api_key` varchar(100) NOT NULL COMMENT '账号id', | ||||
|     `api_secret` varchar(100) NOT NULL COMMENT '账号秘钥', | ||||
|     `api_signature_id` varchar(100) NOT NULL COMMENT '实际渠道签名唯一标识', | ||||
|     `name` varchar(50) not null COMMENT '名称', | ||||
|     `signature` varchar(50) not null COMMENT '签名值', | ||||
|     `remark` varchar(200) NOT NULL COMMENT '备注', | ||||
|  | ||||
|     `status` tinyint(4) NOT NULL default 0 COMMENT '启用状态(0正常 1停用)', | ||||
|     `create_by` varchar(64) not null DEFAULT '' COMMENT '创建者', | ||||
|     `create_time` datetime DEFAULT NULL COMMENT '创建时间', | ||||
|     `update_by` varchar(64) DEFAULT '' COMMENT '更新者', | ||||
|     `update_time` datetime DEFAULT NULL COMMENT '更新时间', | ||||
|     `deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', | ||||
|     PRIMARY KEY (`id`) USING BTREE | ||||
| ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='短信渠道'; | ||||
| /* | ||||
| 	优先级值一样时,按照id顺序取值 | ||||
| */ | ||||
|  | ||||
| -- ---------------------------- | ||||
| -- Table structure for sms_template | ||||
| -- ---------------------------- | ||||
| DROP TABLE IF EXISTS `sms_template`; | ||||
| CREATE TABLE `sms_template` ( | ||||
|     `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增编号', | ||||
|     `channel_code` varchar(50) not null COMMENT '短信渠道编码(来自枚举类)', | ||||
|     `channel_id` bigint(20) not null COMMENT '短信渠道id (对于前端来说就是绑定一个签名)', | ||||
|     `type` tinyint(4) NOT NULL default 1 COMMENT '消息类型 [0验证码 1短信通知 2推广短信 3国际/港澳台消息]', | ||||
|     `biz_code` varchar(50) not null COMMENT '业务编码(来自数据字典, 用户自定义业务场景 一个场景可以有多个模板)', | ||||
|     `code` varchar(50) not null COMMENT '编码', | ||||
|     `name` varchar(50) not null COMMENT '名称', | ||||
|     `api_template_id` varchar(100) NOT NULL COMMENT '实际渠道模板唯一标识', | ||||
|     `content` varchar(1000) NOT NULL DEFAULT '' COMMENT '内容', | ||||
|     `params` varchar(200) NOT NULL DEFAULT '' COMMENT '参数数组(自动根据内容生成)', | ||||
|     `remark` varchar(200) NOT NULL COMMENT '备注', | ||||
|  | ||||
|     `status` tinyint(4) NOT NULL default 0 COMMENT '启用状态(0正常 1停用)', | ||||
|     `create_by` varchar(64) not null DEFAULT '' COMMENT '创建者', | ||||
|     `create_time` datetime DEFAULT NULL COMMENT '创建时间', | ||||
|     `update_by` varchar(64) DEFAULT '' COMMENT '更新者', | ||||
|     `update_time` datetime DEFAULT NULL COMMENT '更新时间', | ||||
|     `deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', | ||||
|     PRIMARY KEY (`id`) USING BTREE | ||||
| ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='短信模板'; | ||||
|  | ||||
| -- ---------------------------- | ||||
| -- Table structure for sms_log | ||||
| -- ---------------------------- | ||||
| DROP TABLE IF EXISTS `sms_log`; | ||||
| CREATE TABLE `sms_log` ( | ||||
|     `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增编号', | ||||
|     `channel_code` varchar(50) not null COMMENT '短信渠道编码(来自枚举类)', | ||||
|     `api_sms_id` varchar(50) not null COMMENT '实际渠道短信唯一标识', | ||||
|     `template_id` bigint(20) NOT NULL COMMENT '模板id', | ||||
|     `phone` char(11) not null COMMENT '手机号', | ||||
|     `content` varchar(1000) NOT NULL DEFAULT '' COMMENT '内容', | ||||
|     `remark` varchar(200) NOT NULL COMMENT '备注', | ||||
|     `send_status` tinyint(4) NOT NULL default 0 COMMENT '发送状态(0发送中 1成功 2失败)', | ||||
|     `create_by` varchar(64) not null DEFAULT '' COMMENT '创建者', | ||||
|     `create_time` datetime DEFAULT NULL COMMENT '创建时间', | ||||
|     PRIMARY KEY (`id`) USING BTREE | ||||
| ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='短信日志'; | ||||
|  | ||||
| SET FOREIGN_KEY_CHECKS = 1; | ||||
|   | ||||
							
								
								
									
										82
									
								
								sql/sms.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								sql/sms.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| /* | ||||
|     --2021.02.01 by fight, sms about table info | ||||
| */ | ||||
|  | ||||
| -- ---------------------------- | ||||
| -- Table structure for sms_channel | ||||
| -- ---------------------------- | ||||
| DROP TABLE IF EXISTS `sms_channel`; | ||||
| CREATE TABLE `sms_channel` | ||||
| ( | ||||
|     `id`               bigint(20)   NOT NULL AUTO_INCREMENT COMMENT '自增编号', | ||||
|     `code`             varchar(50)  NOT NULL COMMENT '编码(来自枚举类 阿里、华为、七牛等)', | ||||
|     `api_key`          varchar(100) NOT NULL COMMENT '账号id', | ||||
|     `api_secret`       varchar(100) NOT NULL COMMENT '账号秘钥', | ||||
|     `api_signature_id` varchar(100) NOT NULL COMMENT '实际渠道签名唯一标识', | ||||
|     `name`             varchar(50)  NOT NULL COMMENT '名称', | ||||
|     `signature`        varchar(50)  NOT NULL COMMENT '签名值', | ||||
|     `remark`           varchar(200) NOT NULL COMMENT '备注', | ||||
|  | ||||
|     `status`           tinyint(4)   NOT NULL DEFAULT 0 COMMENT '启用状态(0正常 1停用)', | ||||
|     `create_by`        varchar(64)  NOT NULL DEFAULT '' COMMENT '创建者', | ||||
|     `create_time`      datetime              DEFAULT NULL COMMENT '创建时间', | ||||
|     `update_by`        varchar(64)           DEFAULT '' COMMENT '更新者', | ||||
|     `update_time`      datetime              DEFAULT NULL COMMENT '更新时间', | ||||
|     `deleted`          bit(1)                DEFAULT b'0' COMMENT '是否删除', | ||||
|     PRIMARY KEY (`id`) USING BTREE | ||||
| ) ENGINE = InnoDB | ||||
|   AUTO_INCREMENT = 1 | ||||
|   DEFAULT CHARSET = utf8mb4 COMMENT ='短信渠道'; | ||||
| /* | ||||
| 	优先级值一样时,按照id顺序取值 | ||||
| */ | ||||
|  | ||||
| -- ---------------------------- | ||||
| -- Table structure for sms_template | ||||
| -- ---------------------------- | ||||
| DROP TABLE IF EXISTS `sms_template`; | ||||
| CREATE TABLE `sms_template` | ||||
| ( | ||||
|     `id`              bigint(20)    NOT NULL AUTO_INCREMENT COMMENT '自增编号', | ||||
|     `channel_code`    varchar(50)   NOT NULL COMMENT '短信渠道编码(来自枚举类)', | ||||
|     `channel_id`      bigint(20)    NOT NULL COMMENT '短信渠道id (对于前端来说就是绑定一个签名)', | ||||
|     `type`            tinyint(4)    NOT NULL DEFAULT 1 COMMENT '消息类型 [0验证码 1短信通知 2推广短信 3国际/港澳台消息]', | ||||
|     `biz_code`        varchar(50)   NOT NULL COMMENT '业务编码(来自数据字典, 用户自定义业务场景 一个场景可以有多个模板)', | ||||
|     `code`            varchar(50)   NOT NULL COMMENT '编码', | ||||
|     `name`            varchar(50)   NOT NULL COMMENT '名称', | ||||
|     `api_template_id` varchar(100)  NOT NULL COMMENT '实际渠道模板唯一标识', | ||||
|     `content`         varchar(1000) NOT NULL DEFAULT '' COMMENT '内容', | ||||
|     `params`          varchar(200)  NOT NULL DEFAULT '' COMMENT '参数数组(自动根据内容生成)', | ||||
|     `remark`          varchar(200)  NOT NULL COMMENT '备注', | ||||
|  | ||||
|     `status`          tinyint(4)    NOT NULL DEFAULT 0 COMMENT '启用状态(0正常 1停用)', | ||||
|     `create_by`       varchar(64)   NOT NULL DEFAULT '' COMMENT '创建者', | ||||
|     `create_time`     datetime               DEFAULT NULL COMMENT '创建时间', | ||||
|     `update_by`       varchar(64)            DEFAULT '' COMMENT '更新者', | ||||
|     `update_time`     datetime               DEFAULT NULL COMMENT '更新时间', | ||||
|     `deleted`         bit(1)                 DEFAULT b'0' COMMENT '是否删除', | ||||
|     PRIMARY KEY (`id`) USING BTREE | ||||
| ) ENGINE = InnoDB | ||||
|   AUTO_INCREMENT = 1 | ||||
|   DEFAULT CHARSET = utf8mb4 COMMENT ='短信模板'; | ||||
|  | ||||
| -- ---------------------------- | ||||
| -- Table structure for sms_log | ||||
| -- ---------------------------- | ||||
| DROP TABLE IF EXISTS `sms_log`; | ||||
| CREATE TABLE `sms_log` | ||||
| ( | ||||
|     `id`            bigint(20)    NOT NULL AUTO_INCREMENT COMMENT '自增编号', | ||||
|     `channel_code`  varchar(50)   NOT NULL COMMENT '短信渠道编码(来自枚举类)', | ||||
|     `channel_id`    bigint(20)    NOT NULL COMMENT '短信渠道id', | ||||
|     `template_code` varchar(50)   NOT NULL COMMENT '渠道编码', | ||||
|     `phones`        char(11)      NOT NULL COMMENT '手机号(数组json字符串)', | ||||
|     `content`       varchar(1000) NOT NULL DEFAULT '' COMMENT '内容', | ||||
|     `remark`        varchar(200)  DEFAULT NULL COMMENT '备注', | ||||
|     `send_status`   tinyint(4)    NOT NULL DEFAULT 2 COMMENT '发送状态(1异步推送中 2发送中 3失败 4成功)', | ||||
|     `create_by`     varchar(64)   NOT NULL DEFAULT '' COMMENT '创建者', | ||||
|     `create_time`   datetime               DEFAULT NULL COMMENT '创建时间', | ||||
|     PRIMARY KEY (`id`) USING BTREE | ||||
| ) ENGINE = InnoDB | ||||
|   AUTO_INCREMENT = 1 | ||||
|   DEFAULT CHARSET = utf8mb4 COMMENT ='短信日志'; | ||||
| @@ -1,107 +0,0 @@ | ||||
| package cn.iocoder.dashboard.framework.sms; | ||||
|  | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
|  | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
|  | ||||
| /** | ||||
|  * 短信父接口 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/1/25 14:14 | ||||
|  */ | ||||
| public interface SmsClient<R> { | ||||
|  | ||||
|     /** | ||||
|      * 发送消息 | ||||
|      * | ||||
|      * @param msgBody 消息内容 | ||||
|      * @param targets 发送对象列表 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     SmsResult<R> send(SmsBody msgBody, Collection<String> targets); | ||||
|  | ||||
|     /** | ||||
|      * 发送消息 | ||||
|      * | ||||
|      * @param msgBody 消息内容 | ||||
|      * @param target  发送对象 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     default SmsResult<R> send(SmsBody msgBody, String target) { | ||||
|         if (StringUtils.isBlank(target)) { | ||||
|             return failResult(); | ||||
|         } | ||||
|  | ||||
|         return send(msgBody, Collections.singletonList(target)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 发送消息 | ||||
|      * | ||||
|      * @param msgBody 消息内容 | ||||
|      * @param targets 发送对象列表 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     default SmsResult<R> send(SmsBody msgBody, String... targets) { | ||||
|         if (targets == null) { | ||||
|             return failResult(); | ||||
|         } | ||||
|  | ||||
|         return send(msgBody, Arrays.asList(targets)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * 异步发送消息 | ||||
|      * | ||||
|      * @param msgBody 消息内容 | ||||
|      * @param targets 发送对象列表 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     SmsResult<R> sendAsync(SmsBody msgBody, Collection<String> targets); | ||||
|  | ||||
|     /** | ||||
|      * 异步发送消息 | ||||
|      * | ||||
|      * @param msgBody 消息内容 | ||||
|      * @param target  发送对象 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     default SmsResult<R> sendAsync(SmsBody msgBody, String target) { | ||||
|         if (StringUtils.isBlank(target)) { | ||||
|             return failResult("target must not null."); | ||||
|         } | ||||
|  | ||||
|         return sendAsync(msgBody, Collections.singletonList(target)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 异步发送消息 | ||||
|      * | ||||
|      * @param msgBody 消息内容 | ||||
|      * @param targets 发送对象列表 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     default SmsResult<R> sendAsync(SmsBody msgBody, String... targets) { | ||||
|         if (targets == null) { | ||||
|             return failResult("targets must not null."); | ||||
|         } | ||||
|  | ||||
|         return sendAsync(msgBody, Arrays.asList(targets)); | ||||
|     } | ||||
|  | ||||
|     default SmsResult<R> failResult() { | ||||
|         SmsResult<R> resultBody = new SmsResult<>(); | ||||
|         resultBody.setSuccess(false); | ||||
|         return resultBody; | ||||
|     } | ||||
|  | ||||
|     default SmsResult<R> failResult(String message) { | ||||
|         SmsResult<R> resultBody = failResult(); | ||||
|         resultBody.setMessage(message); | ||||
|         return resultBody; | ||||
|     } | ||||
| } | ||||
| @@ -1,42 +0,0 @@ | ||||
| package cn.iocoder.dashboard.framework.sms; | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.iocoder.dashboard.common.exception.ServiceException; | ||||
|  | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| import java.util.Map; | ||||
|  | ||||
| import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_NOT_INIT; | ||||
|  | ||||
| /** | ||||
|  * 抽象短信客户端工厂 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/1/28 14:01 | ||||
|  */ | ||||
| public class SmsClientAdapter { | ||||
|  | ||||
|     private final Map<Long, SmsClient<?>> smsSenderMap; | ||||
|  | ||||
|     public SmsClientAdapter(Map<Long, SmsClient<?>> smsSenderMap) { | ||||
|         if (ObjectUtil.isEmpty(smsSenderMap)) { | ||||
|             throw new ServiceException(SMS_CHANNEL_NOT_INIT); | ||||
|         } | ||||
|         this.smsSenderMap = smsSenderMap; | ||||
|     } | ||||
|  | ||||
|     public void flushClient(Map<Long, SmsClient<?>> smsSenderMap) { | ||||
|         this.smsSenderMap.clear(); | ||||
|         smsSenderMap.putAll(Collections.unmodifiableMap(smsSenderMap)); | ||||
|     } | ||||
|  | ||||
|     public SmsResult<?> send(Long channelId, SmsBody smsBody, Collection<String> targetPhone) { | ||||
|         SmsClient<?> smsClient = getSmsSender(channelId); | ||||
|         return smsClient.send(smsBody, targetPhone); | ||||
|     } | ||||
|  | ||||
|     private SmsClient<?> getSmsSender(Long channelId) { | ||||
|         return smsSenderMap.get(channelId); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| package cn.iocoder.dashboard.framework.sms.client; | ||||
|  | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelPropertyVO; | ||||
|  | ||||
| /** | ||||
|  * 抽象短息客户端 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/2/1 9:28 | ||||
|  */ | ||||
| public abstract class AbstractSmsClient<R> implements SmsClient<R> { | ||||
|  | ||||
|     /** | ||||
|      * 短信渠道参数 | ||||
|      */ | ||||
|     protected final SmsChannelPropertyVO channelVO; | ||||
|  | ||||
|     /** | ||||
|      * 构造阿里云短信发送处理 | ||||
|      * | ||||
|      * @param channelVO 阿里云短信配置 | ||||
|      */ | ||||
|     public AbstractSmsClient(SmsChannelPropertyVO channelVO) { | ||||
|         this.channelVO = channelVO; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public SmsChannelPropertyVO getProperty() { | ||||
|         return channelVO; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,9 +1,8 @@ | ||||
| package cn.iocoder.dashboard.modules.system.sms.client; | ||||
| package cn.iocoder.dashboard.framework.sms.client; | ||||
| 
 | ||||
| import cn.iocoder.dashboard.framework.sms.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsResult; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelAllVO; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsResult; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelPropertyVO; | ||||
| import com.aliyuncs.DefaultAcsClient; | ||||
| import com.aliyuncs.IAcsClient; | ||||
| import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; | ||||
| @@ -23,7 +22,7 @@ import java.util.Collection; | ||||
|  * @date 2021/1/25 14:17 | ||||
|  */ | ||||
| @Slf4j | ||||
| public class AliSmsClient implements SmsClient<SendSmsResponse> { | ||||
| public class AliyunSmsClient extends AbstractSmsClient<SendSmsResponse> { | ||||
| 
 | ||||
|     private static final String OK = "OK"; | ||||
| 
 | ||||
| @@ -33,8 +32,6 @@ public class AliSmsClient implements SmsClient<SendSmsResponse> { | ||||
| 
 | ||||
|     private static final String ENDPOINT = "cn-hangzhou"; | ||||
| 
 | ||||
|     private final SmsChannelAllVO channelVO; | ||||
| 
 | ||||
|     private final IAcsClient acsClient; | ||||
| 
 | ||||
|     /** | ||||
| @@ -42,9 +39,8 @@ public class AliSmsClient implements SmsClient<SendSmsResponse> { | ||||
|      * | ||||
|      * @param channelVO 阿里云短信配置 | ||||
|      */ | ||||
|     public AliSmsClient(SmsChannelAllVO channelVO) { | ||||
| 
 | ||||
|         this.channelVO = channelVO; | ||||
|     public AliyunSmsClient(SmsChannelPropertyVO channelVO) { | ||||
|         super(channelVO); | ||||
| 
 | ||||
|         String accessKeyId = channelVO.getApiKey(); | ||||
|         String accessKeySecret = channelVO.getApiSecret(); | ||||
| @@ -57,13 +53,13 @@ public class AliSmsClient implements SmsClient<SendSmsResponse> { | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public SmsResult<SendSmsResponse> send(SmsBody msgBody, Collection<String> targets) { | ||||
|     public SmsResult<SendSmsResponse> send(SmsBody smsBody, Collection<String> targets) { | ||||
|         SendSmsRequest request = new SendSmsRequest(); | ||||
|         request.setSysMethod(MethodType.POST); | ||||
|         request.setPhoneNumbers(StringUtils.join(targets, ",")); | ||||
|         request.setSignName(channelVO.getApiSignatureId()); | ||||
|         request.setTemplateCode(channelVO.getTemplateByTemplateCode(msgBody.getCode()).getApiTemplateId()); | ||||
|         request.setTemplateParam(msgBody.getParamsStr()); | ||||
|         request.setTemplateCode(channelVO.getTemplateByTemplateCode(smsBody.getTemplateCode()).getApiTemplateId()); | ||||
|         request.setTemplateParam(smsBody.getParamsStr()); | ||||
| 
 | ||||
|         try { | ||||
|             SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request); | ||||
| @@ -78,14 +74,15 @@ public class AliSmsClient implements SmsClient<SendSmsResponse> { | ||||
|             return resultBody; | ||||
|         } catch (Exception e) { | ||||
|             log.debug(e.getMessage(), e); | ||||
|             return failResult("发送异常: " + e.getMessage()); | ||||
|         } | ||||
| 
 | ||||
|         return failResult(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public SmsResult<SendSmsResponse> sendAsync(SmsBody msgBody, Collection<String> targets) { | ||||
|         return null; | ||||
|     SmsResult<SendSmsResponse> failResult(String message) { | ||||
|         SmsResult<SendSmsResponse> resultBody = new SmsResult<>(); | ||||
|         resultBody.setSuccess(false); | ||||
|         resultBody.setMessage(message); | ||||
|         return resultBody; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| package cn.iocoder.dashboard.framework.sms.client; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsResult; | ||||
|  | ||||
| import java.util.Collection; | ||||
|  | ||||
| /** | ||||
|  * 短信父接口 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/1/25 14:14 | ||||
|  */ | ||||
| public interface SmsClient<R> { | ||||
|  | ||||
|     /** | ||||
|      * 发送消息 | ||||
|      * | ||||
|      * @param smsBody 消息内容 | ||||
|      * @param targets 发送对象列表 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     SmsResult<R> send(SmsBody smsBody, Collection<String> targets); | ||||
|  | ||||
| } | ||||
| @@ -1,9 +1,10 @@ | ||||
| package cn.iocoder.dashboard.framework.sms; | ||||
| package cn.iocoder.dashboard.framework.sms.core; | ||||
| 
 | ||||
| import cn.iocoder.dashboard.util.json.JsonUtils; | ||||
| import lombok.Data; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| /** | ||||
|  * 消息内容实体类 | ||||
| @@ -11,10 +12,15 @@ import java.util.Map; | ||||
| @Data | ||||
| public class SmsBody { | ||||
| 
 | ||||
|     /** | ||||
|      * 消息日志id | ||||
|      */ | ||||
|     private Long smsLogId; | ||||
| 
 | ||||
|     /** | ||||
|      * 模板编码 | ||||
|      */ | ||||
|     private String code; | ||||
|     private String templateCode; | ||||
| 
 | ||||
|     /** | ||||
|      * 参数列表 | ||||
| @@ -0,0 +1,71 @@ | ||||
| package cn.iocoder.dashboard.framework.sms.core; | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.iocoder.dashboard.common.enums.SmsChannelEnum; | ||||
| import cn.iocoder.dashboard.common.exception.ServiceException; | ||||
| import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; | ||||
| import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.client.AliyunSmsClient; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelPropertyVO; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
|  | ||||
| import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; | ||||
|  | ||||
| /** | ||||
|  * 短信客户端工厂 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/1/28 14:01 | ||||
|  */ | ||||
| @Component | ||||
| public class SmsClientFactory { | ||||
|  | ||||
|     private final Map<Long, AbstractSmsClient<?>> smsSenderMap = new ConcurrentHashMap<>(8); | ||||
|  | ||||
|     /** | ||||
|      * 创建短信客户端 | ||||
|      * | ||||
|      * @param propertyVO 参数对象 | ||||
|      * @return 客户端id(默认channelId) | ||||
|      */ | ||||
|     public Long createClient(SmsChannelPropertyVO propertyVO) { | ||||
|         if (StrUtil.isBlank(propertyVO.getCode())) { | ||||
|             throw ServiceExceptionUtil.exception(PARAM_VALUE_IS_NULL, "短信渠道编码"); | ||||
|         } | ||||
|         if (ObjectUtil.isNull(propertyVO.getId())) { | ||||
|             throw ServiceExceptionUtil.exception(PARAM_VALUE_IS_NULL, "短信渠道ID"); | ||||
|         } | ||||
|  | ||||
|         AbstractSmsClient<?> sender = createClient(SmsChannelEnum.getByCode(propertyVO.getCode()), propertyVO); | ||||
|         smsSenderMap.put(propertyVO.getId(), sender); | ||||
|         return propertyVO.getId(); | ||||
|     } | ||||
|  | ||||
|     private AbstractSmsClient<?> createClient(SmsChannelEnum channelEnum, SmsChannelPropertyVO channelVO) { | ||||
|         if (channelEnum == null) { | ||||
|             throw new ServiceException(INVALID_CHANNEL_CODE); | ||||
|         } | ||||
|         switch (channelEnum) { | ||||
|             case ALI: | ||||
|                 return new AliyunSmsClient(channelVO); | ||||
|             // TODO fill more channel | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|         throw new ServiceException(SMS_SENDER_NOT_FOUND); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取短信客户端 | ||||
|      * | ||||
|      * @param channelId 渠道id | ||||
|      * @return 短信id | ||||
|      */ | ||||
|     public AbstractSmsClient<?> getClient(Long channelId) { | ||||
|         return smsSenderMap.get(channelId); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package cn.iocoder.dashboard.framework.sms; | ||||
| package cn.iocoder.dashboard.framework.sms.core; | ||||
| 
 | ||||
| import lombok.Data; | ||||
| 
 | ||||
| @@ -44,11 +44,5 @@ public class SmsChannelController { | ||||
|         return success(service.createChannel(reqVO)); | ||||
|     } | ||||
|  | ||||
|     @ApiOperation("刷新消息渠道信息") | ||||
|     @PutMapping("/flush") | ||||
|     public CommonResult<Boolean> flushChannel() { | ||||
|         return success(service.flushChannel()); | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import java.util.List; | ||||
|  */ | ||||
| @Data | ||||
| @EqualsAndHashCode | ||||
| public class SmsChannelAllVO implements Serializable { | ||||
| public class SmsChannelPropertyVO implements Serializable { | ||||
| 
 | ||||
|     /** | ||||
|      * id | ||||
| @@ -17,6 +17,7 @@ public class SmsTemplateVO { | ||||
|      * 业务编码(来自数据字典, 用户自定义业务场景 一个场景可以有多个模板) | ||||
|      */ | ||||
|     private String bizCode; | ||||
|  | ||||
|     /** | ||||
|      * 编码 | ||||
|      */ | ||||
| @@ -27,4 +28,11 @@ public class SmsTemplateVO { | ||||
|      */ | ||||
|     private String apiTemplateId; | ||||
|  | ||||
|     /** | ||||
|      * 内容 | ||||
|      */ | ||||
|     private String content; | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ package cn.iocoder.dashboard.modules.system.convert.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.common.enums.SmsChannelEnum; | ||||
| import cn.iocoder.dashboard.common.pojo.PageResult; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelAllVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelPropertyVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.req.SmsChannelCreateReqVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.resp.SmsChannelEnumRespVO; | ||||
| import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SmsChannelDO; | ||||
| @@ -28,7 +28,7 @@ public interface SmsChannelConvert { | ||||
|  | ||||
|     List<SmsChannelEnumRespVO> convertEnum(List<SmsChannelEnum> bean); | ||||
|  | ||||
|     List<SmsChannelAllVO> convert(List<SmsChannelDO> bean); | ||||
|     List<SmsChannelPropertyVO> convert(List<SmsChannelDO> bean); | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| package cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SmsLog; | ||||
| import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SmsLogDO; | ||||
| import com.baomidou.mybatisplus.core.mapper.BaseMapper; | ||||
| import org.apache.ibatis.annotations.Mapper; | ||||
|  | ||||
| @Mapper | ||||
| public interface SmsLogMapper extends BaseMapper<SmsLog> { | ||||
| public interface SmsLogMapper extends BaseMapper<SmsLogDO> { | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms; | ||||
| import com.baomidou.mybatisplus.annotation.TableName; | ||||
| import lombok.Data; | ||||
| import lombok.EqualsAndHashCode; | ||||
| import lombok.experimental.Accessors; | ||||
| 
 | ||||
| import java.io.Serializable; | ||||
| import java.util.Date; | ||||
| @@ -15,8 +16,9 @@ import java.util.Date; | ||||
|  */ | ||||
| @Data | ||||
| @EqualsAndHashCode | ||||
| @Accessors(chain = true) | ||||
| @TableName(value = "sms_log", autoResultMap = true) | ||||
| public class SmsLog implements Serializable { | ||||
| public class SmsLogDO implements Serializable { | ||||
| 
 | ||||
|     /** | ||||
|      * 自增编号 | ||||
| @@ -29,19 +31,19 @@ public class SmsLog implements Serializable { | ||||
|     private String channelCode; | ||||
| 
 | ||||
|     /** | ||||
|      * 实际渠道短信唯一标识 | ||||
|      * 短信渠道id | ||||
|      */ | ||||
|     private String apiSmsId; | ||||
|     private Long channelId; | ||||
| 
 | ||||
|     /** | ||||
|      * 模板id | ||||
|      */ | ||||
|     private Long templateId; | ||||
|     private String templateCode; | ||||
| 
 | ||||
|     /** | ||||
|      * 手机号 | ||||
|      * 手机号(数组json字符串) | ||||
|      */ | ||||
|     private String phone; | ||||
|     private String phones; | ||||
| 
 | ||||
|     /** | ||||
|      * 内容 | ||||
| @@ -54,7 +56,7 @@ public class SmsLog implements Serializable { | ||||
|     private String remark; | ||||
| 
 | ||||
|     /** | ||||
|      * 发送状态(0发送中 1成功 2失败) | ||||
|      * 发送状态(1异步推送中 2发送中 3失败 4成功) | ||||
|      */ | ||||
|     private Integer sendStatus; | ||||
| 
 | ||||
| @@ -83,5 +83,6 @@ public interface SysErrorCodeConstants { | ||||
|     ErrorCode SMS_TEMPLATE_NOT_FOUND = new ErrorCode(1003001003, "没有短信模板信息, 请初始化sms_template表数据。"); | ||||
|     ErrorCode SMS_SENDER_NOT_FOUND = new ErrorCode(1003001004, "没有找到对应的短信发送对象,请检查sms_channel表和sms_template表数据"); | ||||
|     ErrorCode INVALID_CHANNEL_CODE = new ErrorCode(1003001005, "非法的短信渠道code,请检查sms_channel表的code值是否与SmsChannelEnum中的code值一致。"); | ||||
|     ErrorCode PARAM_VALUE_IS_NULL = new ErrorCode(1003001006, "参数【{}】不能为空"); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,30 @@ | ||||
| package cn.iocoder.dashboard.modules.system.enums.sms; | ||||
|  | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Getter; | ||||
|  | ||||
| /** | ||||
|  * 短信发送状态 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/2/1 13:39 | ||||
|  */ | ||||
| @Getter | ||||
| @AllArgsConstructor | ||||
| public enum SmsSendStatusEnum { | ||||
|  | ||||
|     //异步转发中 | ||||
|     ASYNC(1), | ||||
|  | ||||
|     //发送中 | ||||
|     SENDING(2), | ||||
|  | ||||
|     //失败 | ||||
|     FAIL(3), | ||||
|  | ||||
|     //成功 | ||||
|     SUCCESS(4); | ||||
|  | ||||
|     private final int status; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| package cn.iocoder.dashboard.modules.system.mq.consumer.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsResult; | ||||
| import cn.iocoder.dashboard.modules.system.mq.message.dept.SysDeptRefreshMessage; | ||||
| import cn.iocoder.dashboard.modules.system.mq.message.sms.SmsSendMessage; | ||||
| import cn.iocoder.dashboard.modules.system.service.sms.SmsService; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
|  | ||||
| /** | ||||
|  * 针对 {@link SysDeptRefreshMessage} 的消费者 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Component | ||||
| @Slf4j | ||||
| public class SmsSendConsumer extends AbstractChannelMessageListener<SmsSendMessage> { | ||||
|  | ||||
|     @Resource | ||||
|     private SmsService smsService; | ||||
|  | ||||
|     @Override | ||||
|     public void onMessage(SmsSendMessage message) { | ||||
|         log.info("[onMessage][收到 发送短信 消息]"); | ||||
|         SmsResult<?> send = smsService.send(message.getSmsBody(), message.getTargetPhones()); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| package cn.iocoder.dashboard.modules.system.mq.message.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsBody; | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.util.Collection; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 部门数据刷新 Message | ||||
|  */ | ||||
| @Data | ||||
| public class SmsSendMessage implements ChannelMessage { | ||||
|  | ||||
|     private SmsBody smsBody; | ||||
|  | ||||
|     private List<String> targetPhones; | ||||
|  | ||||
|     @Override | ||||
|     public String getChannel() { | ||||
|         return "sms.send"; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| package cn.iocoder.dashboard.modules.system.mq.producer.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsBody; | ||||
| import cn.iocoder.dashboard.modules.system.mq.message.sms.SmsSendMessage; | ||||
| import org.springframework.data.redis.core.StringRedisTemplate; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 短信的 Producer | ||||
|  */ | ||||
| @Component | ||||
| public class SmsProducer { | ||||
|  | ||||
|     @Resource | ||||
|     private StringRedisTemplate stringRedisTemplate; | ||||
|  | ||||
|     /** | ||||
|      * 发送 {@link SmsSendMessage} 消息 | ||||
|      */ | ||||
|     public void sendSmsSendMessage(SmsBody smsBody, List<String> targetPhoneList) { | ||||
|         SmsSendMessage message = new SmsSendMessage(); | ||||
|         message.setSmsBody(smsBody); | ||||
|         message.setTargetPhones(targetPhoneList); | ||||
|         RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,7 +1,8 @@ | ||||
| package cn.iocoder.dashboard.modules.system.service.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.common.pojo.PageResult; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelAllVO; | ||||
| import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelPropertyVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.req.SmsChannelCreateReqVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.req.SmsChannelPageReqVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.resp.SmsChannelEnumRespVO; | ||||
| @@ -17,18 +18,46 @@ import java.util.List; | ||||
|  */ | ||||
| public interface SmsChannelService { | ||||
|  | ||||
|     /** | ||||
|      * 初始化短信渠道 | ||||
|      */ | ||||
|     void initSmsClient(); | ||||
|  | ||||
|     /** | ||||
|      * 分页查询短信渠道信息 | ||||
|      * | ||||
|      * @param reqVO 参数对象 | ||||
|      * @return 短信渠道分页对象 | ||||
|      */ | ||||
|     PageResult<SmsChannelDO> pageChannels(SmsChannelPageReqVO reqVO); | ||||
|  | ||||
|     /** | ||||
|      * 创建新的渠道信息 | ||||
|      * | ||||
|      * @param reqVO 参数对象 | ||||
|      * @return 渠道id | ||||
|      */ | ||||
|     Long createChannel(SmsChannelCreateReqVO reqVO); | ||||
|  | ||||
|     /** | ||||
|      * 获取短信渠道枚举/渠道编码 | ||||
|      * | ||||
|      * @return 短信渠道枚举/渠道编码 | ||||
|      */ | ||||
|     List<SmsChannelEnumRespVO> getChannelEnums(); | ||||
|  | ||||
|     /** | ||||
|      * 根据短信模板编码获取短信客户端 | ||||
|      * | ||||
|      * @param templateCode 短信模板编码 | ||||
|      * @return 短信客户端 | ||||
|      */ | ||||
|     AbstractSmsClient<?> getClient(String templateCode); | ||||
|  | ||||
|     /** | ||||
|      * 查询渠道(包含名下模块)信息集合 | ||||
|      * | ||||
|      * @return 渠道(包含名下模块)信息集合 | ||||
|      */ | ||||
|     List<SmsChannelAllVO> listChannelAllEnabledInfo(); | ||||
|  | ||||
|     boolean flushChannel(); | ||||
|     List<SmsChannelPropertyVO> listChannelAllEnabledInfo(); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| package cn.iocoder.dashboard.modules.system.service.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsResult; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 短信渠道Service接口 | ||||
|  * | ||||
| @@ -7,4 +13,23 @@ package cn.iocoder.dashboard.modules.system.service.sms; | ||||
|  * @date 2021/1/25 9:24 | ||||
|  */ | ||||
| public interface SmsLogService { | ||||
|     /** | ||||
|      * 发送短信前的日志处理 | ||||
|      * | ||||
|      * @param smsBody      短信内容 | ||||
|      * @param targetPhones 发送对象手机号集合 | ||||
|      * @param client       短信客户端 | ||||
|      * @param isAsync      是否异步发送 | ||||
|      * @return 生成的日志id | ||||
|      */ | ||||
|     Long beforeSendLog(SmsBody smsBody, List<String> targetPhones, AbstractSmsClient<?> client, Boolean isAsync); | ||||
|  | ||||
|     /** | ||||
|      * 发送消息后的日志处理 | ||||
|      * | ||||
|      * @param logId  日志id | ||||
|      * @param result 消息结果 | ||||
|      */ | ||||
|     void afterSendLog(Long logId, SmsResult<?> result); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,101 @@ | ||||
| package cn.iocoder.dashboard.modules.system.service.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsResult; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
|  | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 短信Service接口 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/1/25 9:24 | ||||
|  */ | ||||
| public interface SmsService { | ||||
|  | ||||
|     /** | ||||
|      * 发送消息 | ||||
|      * | ||||
|      * @param smsBody      消息内容 | ||||
|      * @param targetPhones 发送对象手机号列表 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     SmsResult<?> send(SmsBody smsBody, List<String> targetPhones); | ||||
|  | ||||
|     /** | ||||
|      * 发送消息 | ||||
|      * | ||||
|      * @param smsBody     消息内容 | ||||
|      * @param targetPhone 发送对象手机号 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     default SmsResult<?> send(SmsBody smsBody, String targetPhone) { | ||||
|         if (StringUtils.isBlank(targetPhone)) { | ||||
|             return failResult("targetPhone must not null."); | ||||
|         } | ||||
|  | ||||
|         return send(smsBody, Collections.singletonList(targetPhone)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 发送消息 | ||||
|      * | ||||
|      * @param smsBody      消息内容 | ||||
|      * @param targetPhones 发送对象手机号数组 | ||||
|      * @return 是否发送成功 | ||||
|      */ | ||||
|     default SmsResult<?> send(SmsBody smsBody, String... targetPhones) { | ||||
|         if (targetPhones == null) { | ||||
|             return failResult("targetPhones must not null."); | ||||
|         } | ||||
|  | ||||
|         return send(smsBody, Arrays.asList(targetPhones)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * 异步发送消息 | ||||
|      * | ||||
|      * @param msgBody      消息内容 | ||||
|      * @param targetPhones 发送对象列表 | ||||
|      */ | ||||
|     void sendAsync(SmsBody msgBody, List<String> targetPhones); | ||||
|  | ||||
|     /** | ||||
|      * 异步发送消息 | ||||
|      * | ||||
|      * @param msgBody     消息内容 | ||||
|      * @param targetPhone 发送对象 | ||||
|      */ | ||||
|     default void sendAsync(SmsBody msgBody, String targetPhone) { | ||||
|         if (StringUtils.isBlank(targetPhone)) { | ||||
|             return; | ||||
|         } | ||||
|         sendAsync(msgBody, Collections.singletonList(targetPhone)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 异步发送消息 | ||||
|      * | ||||
|      * @param msgBody      消息内容 | ||||
|      * @param targetPhones 发送对象列表 | ||||
|      */ | ||||
|     default void sendAsync(SmsBody msgBody, String... targetPhones) { | ||||
|         if (targetPhones == null) { | ||||
|             return; | ||||
|         } | ||||
|         sendAsync(msgBody, Arrays.asList(targetPhones)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     default SmsResult<?> failResult(String message) { | ||||
|         SmsResult<?> resultBody = new SmsResult<>(); | ||||
|         resultBody.setSuccess(false); | ||||
|         resultBody.setMessage(message); | ||||
|         return resultBody; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -3,7 +3,9 @@ package cn.iocoder.dashboard.modules.system.service.sms.impl; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.iocoder.dashboard.common.enums.SmsChannelEnum; | ||||
| import cn.iocoder.dashboard.common.pojo.PageResult; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelAllVO; | ||||
| import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsClientFactory; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelPropertyVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.req.SmsChannelCreateReqVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.req.SmsChannelPageReqVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.resp.SmsChannelEnumRespVO; | ||||
| @@ -14,12 +16,16 @@ import cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms.SmsTemplateMapper; | ||||
| import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SmsChannelDO; | ||||
| import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SmsTemplateDO; | ||||
| import cn.iocoder.dashboard.modules.system.service.sms.SmsChannelService; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import javax.annotation.PostConstruct; | ||||
| import javax.annotation.Resource; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
|  | ||||
| /** | ||||
|  * 短信渠道Service实现类 | ||||
| @@ -30,6 +36,30 @@ import java.util.List; | ||||
| @Service | ||||
| public class SmsChannelServiceImpl implements SmsChannelService { | ||||
|  | ||||
|     private final Map<String, Long> templateCode2ChannelIdMap = new ConcurrentHashMap<>(32); | ||||
|  | ||||
|     @Autowired | ||||
|     private SmsClientFactory smsClientFactory; | ||||
|  | ||||
|     /** | ||||
|      * 初始化短信客户端 | ||||
|      */ | ||||
|     @PostConstruct | ||||
|     @Override | ||||
|     public void initSmsClient() { | ||||
|         List<SmsChannelPropertyVO> smsChannelPropertyVOList = listChannelAllEnabledInfo(); | ||||
|         if (ObjectUtil.isEmpty(smsChannelPropertyVOList)) { | ||||
|             return; | ||||
|         } | ||||
|         smsChannelPropertyVOList.forEach(smsChannelPropertyVO -> { | ||||
|             Long clientId = smsClientFactory.createClient(smsChannelPropertyVO); | ||||
|             smsChannelPropertyVO.getTemplateList().forEach(smsTemplateVO -> { | ||||
|                 templateCode2ChannelIdMap.put(smsTemplateVO.getCode(), clientId); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @Resource | ||||
|     private SmsChannelMapper mapper; | ||||
|  | ||||
| @@ -54,12 +84,17 @@ public class SmsChannelServiceImpl implements SmsChannelService { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<SmsChannelAllVO> listChannelAllEnabledInfo() { | ||||
|     public AbstractSmsClient<?> getClient(String templateCode) { | ||||
|         return smsClientFactory.getClient(templateCode2ChannelIdMap.get(templateCode)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<SmsChannelPropertyVO> listChannelAllEnabledInfo() { | ||||
|         List<SmsChannelDO> channelDOList = mapper.selectEnabledList(); | ||||
|         if (ObjectUtil.isNull(channelDOList)) { | ||||
|             return null; | ||||
|         } | ||||
|         List<SmsChannelAllVO> channelAllVOList = SmsChannelConvert.INSTANCE.convert(channelDOList); | ||||
|         List<SmsChannelPropertyVO> channelAllVOList = SmsChannelConvert.INSTANCE.convert(channelDOList); | ||||
|  | ||||
|         channelAllVOList.forEach(smsChannelDO -> { | ||||
|  | ||||
| @@ -71,10 +106,4 @@ public class SmsChannelServiceImpl implements SmsChannelService { | ||||
|         }); | ||||
|         return channelAllVOList; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean flushChannel() { | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,21 @@ | ||||
| package cn.iocoder.dashboard.modules.system.service.sms.impl; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsResult; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelPropertyVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsTemplateVO; | ||||
| import cn.iocoder.dashboard.modules.system.dal.mysql.dao.sms.SmsLogMapper; | ||||
| import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.sms.SmsLogDO; | ||||
| import cn.iocoder.dashboard.modules.system.enums.sms.SmsSendStatusEnum; | ||||
| import cn.iocoder.dashboard.modules.system.service.sms.SmsLogService; | ||||
| import cn.iocoder.dashboard.util.json.JsonUtils; | ||||
| import cn.iocoder.dashboard.util.string.StrUtils; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 短信日志Service实现类 | ||||
|  * | ||||
| @@ -12,4 +25,48 @@ import org.springframework.stereotype.Service; | ||||
| @Service | ||||
| public class SmsLogServiceImpl implements SmsLogService { | ||||
|  | ||||
|     @Resource | ||||
|     private SmsLogMapper smsLogMapper; | ||||
|  | ||||
|     @Override | ||||
|     public Long beforeSendLog(SmsBody smsBody, List<String> targetPhones, AbstractSmsClient<?> client, Boolean isAsync) { | ||||
|         SmsLogDO smsLog = new SmsLogDO(); | ||||
|         if (smsBody.getSmsLogId() != null) { | ||||
|             smsLog.setId(smsBody.getSmsLogId()); | ||||
|             smsLog.setSendStatus(SmsSendStatusEnum.SENDING.getStatus()); | ||||
|             smsLogMapper.updateById(smsLog); | ||||
|             return smsBody.getSmsLogId(); | ||||
|         } else { | ||||
|             SmsChannelPropertyVO property = client.getProperty(); | ||||
|             SmsTemplateVO smsTemplate = property.getTemplateByTemplateCode(smsBody.getTemplateCode()); | ||||
|  | ||||
|             smsLog.setChannelCode(property.getCode()) | ||||
|                     .setChannelId(property.getId()) | ||||
|                     .setTemplateCode(smsTemplate.getCode()) | ||||
|                     .setPhones(JsonUtils.toJsonString(targetPhones)) | ||||
|                     .setContent(StrUtils.replace(smsTemplate.getContent(), smsBody.getParams())); | ||||
|  | ||||
|             if (isAsync) { | ||||
|                 smsLog.setSendStatus(SmsSendStatusEnum.ASYNC.getStatus()); | ||||
|             } else { | ||||
|                 smsLog.setSendStatus(SmsSendStatusEnum.SENDING.getStatus()); | ||||
|             } | ||||
|             smsLogMapper.insert(smsLog); | ||||
|             return smsLog.getId(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void afterSendLog(Long logId, SmsResult<?> result) { | ||||
|         SmsLogDO smsLog = new SmsLogDO(); | ||||
|         smsLog.setId(logId); | ||||
|         if (result.getSuccess()) { | ||||
|             smsLog.setSendStatus(SmsSendStatusEnum.SUCCESS.getStatus()); | ||||
|         } else { | ||||
|             smsLog.setSendStatus(SmsSendStatusEnum.FAIL.getStatus()); | ||||
|             smsLog.setRemark(result.getMessage() + JsonUtils.toJsonString(result.getResult())); | ||||
|         } | ||||
|         smsLogMapper.updateById(smsLog); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,51 @@ | ||||
| package cn.iocoder.dashboard.modules.system.service.sms.impl; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.core.SmsResult; | ||||
| import cn.iocoder.dashboard.modules.system.mq.producer.sms.SmsProducer; | ||||
| import cn.iocoder.dashboard.modules.system.service.sms.SmsChannelService; | ||||
| import cn.iocoder.dashboard.modules.system.service.sms.SmsLogService; | ||||
| import cn.iocoder.dashboard.modules.system.service.sms.SmsService; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 短信日志Service实现类 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/1/25 9:25 | ||||
|  */ | ||||
| @Service | ||||
| public class SmsServiceImpl implements SmsService { | ||||
|  | ||||
|     @Resource | ||||
|     private SmsChannelService channelService; | ||||
|  | ||||
|     @Resource | ||||
|     private SmsLogService smsLogService; | ||||
|  | ||||
|     @Resource | ||||
|     private SmsProducer smsProducer; | ||||
|  | ||||
|     @Override | ||||
|     public SmsResult<?> send(SmsBody smsBody, List<String> targetPhones) { | ||||
|         AbstractSmsClient<?> client = channelService.getClient(smsBody.getTemplateCode()); | ||||
|         Long logId = smsLogService.beforeSendLog(smsBody, targetPhones, client, false); | ||||
|  | ||||
|         SmsResult<?> result = client.send(smsBody, targetPhones); | ||||
|  | ||||
|         smsLogService.afterSendLog(logId, result); | ||||
|  | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void sendAsync(SmsBody smsBody, List<String> targetPhones) { | ||||
|         AbstractSmsClient<?> client = channelService.getClient(smsBody.getTemplateCode()); | ||||
|         smsLogService.beforeSendLog(smsBody, targetPhones, client, true); | ||||
|         smsProducer.sendSmsSendMessage(smsBody, targetPhones); | ||||
|     } | ||||
| } | ||||
| @@ -1,34 +0,0 @@ | ||||
| package cn.iocoder.dashboard.modules.system.sms; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.sms.SmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsClientAdapter; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelAllVO; | ||||
| import cn.iocoder.dashboard.modules.system.service.sms.SmsChannelService; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 短信服务配置 | ||||
|  * | ||||
|  * @author guer | ||||
|  */ | ||||
| @Configuration | ||||
| @ConditionalOnProperty("sms.enabled") | ||||
| public class SmsConfiguration { | ||||
|  | ||||
|     @Resource | ||||
|     private SmsChannelService channelService; | ||||
|  | ||||
|     @Bean | ||||
|     public SmsClientAdapter smsClientWrapper() { | ||||
|         List<SmsChannelAllVO> smsChannelAllVOList = channelService.listChannelAllEnabledInfo(); | ||||
|         Map<Long, SmsClient<?>> channelId2SmsClientMap = SmsSenderUtils.init(smsChannelAllVOList); | ||||
|         return new SmsClientAdapter(channelId2SmsClientMap); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,143 +0,0 @@ | ||||
| package cn.iocoder.dashboard.modules.system.sms; | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.iocoder.dashboard.common.enums.SmsChannelEnum; | ||||
| import cn.iocoder.dashboard.common.exception.ServiceException; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsClientAdapter; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsResult; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsChannelAllVO; | ||||
| import cn.iocoder.dashboard.modules.system.controller.sms.vo.SmsTemplateVO; | ||||
| import cn.iocoder.dashboard.modules.system.sms.client.AliSmsClient; | ||||
| import cn.iocoder.dashboard.modules.system.sms.proxy.SmsClientLogProxy; | ||||
|  | ||||
| import java.util.*; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
|  | ||||
| import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; | ||||
|  | ||||
| /** | ||||
|  * 短信发送者工厂 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/1/25 16:18 | ||||
|  */ | ||||
| public class SmsSenderUtils { | ||||
|  | ||||
|     /** | ||||
|      * 短信渠道id:短信客户端map | ||||
|      * key: channelId | ||||
|      * val: SmsClient | ||||
|      */ | ||||
|     private static final Map<Long, SmsClient<?>> smsSenderMap = new ConcurrentHashMap<>(8); | ||||
|  | ||||
|     /** | ||||
|      * 短信模板code: 短信渠道id map | ||||
|      * key: templateCode | ||||
|      * val: channelId | ||||
|      */ | ||||
|     private static final Map<String, Long> templateCode2ChannelIdMap = new HashMap<>(); | ||||
|  | ||||
|     /** | ||||
|      * 将短信渠道信息初始化成短信客户端 | ||||
|      * | ||||
|      * @param smsChannelAllVOList 短信渠道信息 | ||||
|      * @return 短信渠道id:短信客户端map | ||||
|      */ | ||||
|     public synchronized static Map<Long, SmsClient<?>> init(List<SmsChannelAllVO> smsChannelAllVOList) { | ||||
|         if (ObjectUtil.isEmpty(smsChannelAllVOList)) { | ||||
|             throw new ServiceException(SMS_CHANNEL_NOT_FOUND); | ||||
|         } | ||||
|         addSender(smsChannelAllVOList); | ||||
|         return smsSenderMap; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 重置短信客户端信息 | ||||
|      * | ||||
|      * @param smsClientAdapter    短信客户端适配器 | ||||
|      * @param smsChannelAllVOList 短信渠道信息集合 | ||||
|      */ | ||||
|     public synchronized static void flush(SmsClientAdapter smsClientAdapter, List<SmsChannelAllVO> smsChannelAllVOList) { | ||||
|         smsSenderMap.clear(); | ||||
|         smsClientAdapter.flushClient(init(smsChannelAllVOList)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 发送短信 | ||||
|      * | ||||
|      * @param smsClientAdapter 短信客户端适配器 | ||||
|      * @param smsBody          短信内容 | ||||
|      * @param targetPhones     对象手机集合 | ||||
|      * @return 短信发送结果 | ||||
|      */ | ||||
|     public static SmsResult<?> send(SmsClientAdapter smsClientAdapter, SmsBody smsBody, Collection<String> targetPhones) { | ||||
|         Long channelId = templateCode2ChannelIdMap.get(smsBody.getCode()); | ||||
|         if (channelId == null) { | ||||
|             throw new ServiceException(SMS_SENDER_NOT_FOUND); | ||||
|         } | ||||
|         return smsClientAdapter.send(channelId, smsBody, targetPhones); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 发送短信 | ||||
|      * | ||||
|      * @param smsClientAdapter 短信客户端适配器 | ||||
|      * @param smsBody          短信内容 | ||||
|      * @param targetPhone      对象手机 | ||||
|      * @return 短信发送结果 | ||||
|      */ | ||||
|     public static SmsResult<?> send(SmsClientAdapter smsClientAdapter, SmsBody smsBody, String targetPhone) { | ||||
|         Long channelId = templateCode2ChannelIdMap.get(smsBody.getCode()); | ||||
|         if (channelId == null) { | ||||
|             throw new ServiceException(SMS_SENDER_NOT_FOUND); | ||||
|         } | ||||
|         return smsClientAdapter.send(channelId, smsBody, Collections.singletonList(targetPhone)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 发送短信 | ||||
|      * | ||||
|      * @param smsClientAdapter 短信客户端适配器 | ||||
|      * @param smsBody          短信内容 | ||||
|      * @param targetPhones     对象手机数组 | ||||
|      * @return 短信发送结果 | ||||
|      */ | ||||
|     public static SmsResult<?> send(SmsClientAdapter smsClientAdapter, SmsBody smsBody, String... targetPhones) { | ||||
|         Long channelId = templateCode2ChannelIdMap.get(smsBody.getCode()); | ||||
|         if (channelId == null) { | ||||
|             throw new ServiceException(SMS_SENDER_NOT_FOUND); | ||||
|         } | ||||
|         return smsClientAdapter.send(channelId, smsBody, Arrays.asList(targetPhones)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private static void addSender(List<SmsChannelAllVO> smsChannelAllVOList) { | ||||
|         smsChannelAllVOList.forEach(channelAllVO -> addSender(SmsChannelEnum.getByCode(channelAllVO.getCode()), channelAllVO)); | ||||
|     } | ||||
|  | ||||
|     private static void addSender(SmsChannelEnum channelEnum, SmsChannelAllVO channelAllVO) { | ||||
|         if (channelEnum == null) { | ||||
|             throw new ServiceException(INVALID_CHANNEL_CODE); | ||||
|         } | ||||
|         List<SmsTemplateVO> templateList = channelAllVO.getTemplateList(); | ||||
|         if (ObjectUtil.isEmpty(templateList)) { | ||||
|             throw new ServiceException(SMS_TEMPLATE_NOT_FOUND); | ||||
|         } | ||||
|         SmsClient<?> aliSmsClient = getSender(channelEnum, channelAllVO); | ||||
|         templateList.forEach(smsTemplateVO -> templateCode2ChannelIdMap.put(smsTemplateVO.getCode(), channelAllVO.getId())); | ||||
|         smsSenderMap.put(channelAllVO.getId(), aliSmsClient); | ||||
|     } | ||||
|  | ||||
|     private static SmsClient<?> getSender(SmsChannelEnum channelEnum, SmsChannelAllVO channelAllVO) { | ||||
|         switch (channelEnum) { | ||||
|             case ALI: | ||||
|                 return new SmsClientLogProxy<>(new AliSmsClient(channelAllVO)); | ||||
|             // TODO fill more channel | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|         throw new ServiceException(SMS_SENDER_NOT_FOUND); | ||||
|     } | ||||
| } | ||||
| @@ -1,48 +0,0 @@ | ||||
| package cn.iocoder.dashboard.modules.system.sms.proxy; | ||||
|  | ||||
| import cn.iocoder.dashboard.framework.sms.SmsBody; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsClient; | ||||
| import cn.iocoder.dashboard.framework.sms.SmsResult; | ||||
| import cn.iocoder.dashboard.util.json.JsonUtils; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
|  | ||||
| import java.util.Collection; | ||||
|  | ||||
| /** | ||||
|  * 消息父接口 | ||||
|  * | ||||
|  * @author zzf | ||||
|  * @date 2021/1/22 15:46 | ||||
|  */ | ||||
| @Slf4j | ||||
| public class SmsClientLogProxy<R> implements SmsClient<R> { | ||||
|  | ||||
|     private final SmsClient<R> smsClient; | ||||
|  | ||||
|     @Override | ||||
|     public SmsResult<R> send(SmsBody msgBody, Collection<String> targets) { | ||||
|         log.debug("ready send sms, body: {}, target: {}", JsonUtils.toJsonString(msgBody), targets); | ||||
|  | ||||
|         SmsResult<R> resultBody = smsClient.send(msgBody, targets); | ||||
|  | ||||
|         if (resultBody.getSuccess()) { | ||||
|             // | ||||
|         } else { | ||||
|             log.warn("send sms fail, body: {}, target: {}, resultBody: {}", | ||||
|                     JsonUtils.toJsonString(msgBody), | ||||
|                     targets, | ||||
|                     JsonUtils.toJsonString(resultBody) | ||||
|             ); | ||||
|         } | ||||
|         return resultBody; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public SmsResult<R> sendAsync(SmsBody msgBody, Collection<String> targets) { | ||||
|         return send(msgBody, targets); | ||||
|     } | ||||
|  | ||||
|     public SmsClientLogProxy(SmsClient<R> smsClient) { | ||||
|         this.smsClient = smsClient; | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,10 @@ | ||||
| package cn.iocoder.dashboard.util.string; | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 字符串工具类 | ||||
|  * | ||||
| @@ -13,4 +16,22 @@ public class StrUtils { | ||||
|         return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 指定字符串的 | ||||
|      * @param str | ||||
|      * @param replaceMap | ||||
|      * @return | ||||
|      */ | ||||
|     public static String replace(String str, Map<String, String> replaceMap) { | ||||
|         assert StrUtil.isNotBlank(str); | ||||
|         if (ObjectUtil.isEmpty(replaceMap)) { | ||||
|             return str; | ||||
|         } | ||||
|         String result = null; | ||||
|         for (String key : replaceMap.keySet()) { | ||||
|             result = str.replace(key, replaceMap.get(key)); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 zengzefeng
					zengzefeng