Commit 93a4b7eb authored by liuyang's avatar liuyang

短信发送服务

parents
# Created by https://www.gitignore.io/api/eclipse,intellij,maven
### Eclipse ###
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
.DS_Store
.idea
*.iml
# modules.xml
# .idea/misc.xml
# *.ipr
### Maven ###
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
### redis ###
*.rdb
#*.jpg
#*.png
# Exclude maven wrapper
!/.mvn/wrapper/maven-wrapper.jar
# End of https://www.gitignore.io/api/eclipse,intellij,maven
.tomcatplugin
.mvn/
work/*
logs/
src/main/webapp/WEB-INF/classes/
src/main/webapp/WEB-INF/lib/
src/main/webapp/META-INF/MANIFEST.MF
src/main/webapp/upload/
\ No newline at end of file
# sms
短信发送server
\ No newline at end of file
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qkdata</groupId>
<artifactId>online-edu-sms</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>freego-sms</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${mybatis.mapper.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>${mybatis.typehandlers.jsr.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<!-- 阿里云短信服务 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.1.1</version>
<!-- 注:如提示报错,先升级基础包版,无法解决可联系技术支持 -->
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<!--<version>2.6.0</version>-->
</dependency>
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.1.3.RELEASE</version>
<configuration>
<mainClass>${start-class}</mainClass>
<layout>ZIP</layout>
<classifier>all</classifier>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker.plugin.version}</version>
<configuration>
<serverId>harbor</serverId>
<registryUrl>http://${docker.registry}/v2/</registryUrl>
<imageName>${docker.image.prefix}/${project.artifactId}:${project.version}</imageName>
<dockerDirectory>${project.basedir}/src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>*.jar</include>
</resource>
</resources>
<buildArgs>
<JAR_FILE>${project.build.finalName}-all.jar</JAR_FILE>
</buildArgs>
</configuration>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
<execution>
<id>tag-image</id>
<phase>install</phase>
<goals>
<goal>tag</goal>
</goals>
<configuration>
<image>${docker.image.prefix}/${project.artifactId}:${project.version}</image>
<newName>${docker.registry}/${docker.image.prefix}/${project.artifactId}:${project.version}</newName>
</configuration>
</execution>
<execution>
<id>push-image</id>
<phase>install</phase>
<goals>
<goal>push</goal>
</goals>
<configuration>
<imageName>${docker.registry}/${docker.image.prefix}/${project.artifactId}:${project.version}</imageName>
</configuration>
</execution>
<execution>
<id>remove-image</id>
<phase>install</phase>
<goals>
<goal>removeImage</goal>
</goals>
<configuration>
<imageName>${docker.image.prefix}/${project.artifactId}:${project.version}</imageName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<commons-lang3.version>3.1</commons-lang3.version>
<mysql.connector.version>5.1.41</mysql.connector.version>
<druid.springboot.starter.version>1.1.9</druid.springboot.starter.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
<mybatis.pagerhelper.version>1.2.3</mybatis.pagerhelper.version>
<mybatis.mapper.version>2.1.2</mybatis.mapper.version>
<mybatis.typehandlers.jsr.version>1.0.2</mybatis.typehandlers.jsr.version>
<docker.plugin.version>1.1.1</docker.plugin.version>
<docker.registry>fywlsoft.cn:57802</docker.registry>
<docker.image.prefix>argus</docker.image.prefix>
</properties>
</project>
FROM fywlsoft.cn:57802/library/alpine-java:8u202b08_server-jre_unlimited
ARG JAR_FILE
ADD ${JAR_FILE} $WORKDIR
RUN mv ${JAR_FILE} app.jar
COPY docker-entrypoint.sh /usr/share/app/docker-entrypoint.sh
RUN chmod +x /usr/share/app/docker-entrypoint.sh
ENV MYSQL_URL tcp://mysql:3306
ENV WAIT_MYSQL_TIMEOUT 30s
ENV JAVA_OPTIONS "-Xms256m -Xmx512m -Dfile.encoding=UTF-8"
ENV OVERRIDE_PROP ""
ENTRYPOINT ["/usr/share/app/docker-entrypoint.sh"]
EXPOSE 80
HEALTHCHECK --timeout=5s --start-period=60s \
CMD curl -f http://localhost/sms/actuator/health || exit 1
#!/bin/bash
set -ex
java $JAVA_OPTIONS -jar app.jar $OVERRIDE_PROP
package com.qkdata.sms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import tk.mybatis.spring.annotation.MapperScan;
/**
* @author chenjiahai
*/
@SpringBootApplication
@MapperScan("com.qkdata.sms.**.repository")
@EnableScheduling
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package com.qkdata.sms.api;
import com.qkdata.sms.exception.SmsException;
import com.qkdata.sms.service.TemplateService;
import com.qkdata.sms.constant.ResponseData;
import com.qkdata.sms.model.InsertTemplateCondition;
import com.qkdata.sms.model.SmsMessageCondition;
import com.qkdata.sms.model.SmsV1Condition;
import com.qkdata.sms.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.io.IOException;
/**
* @author chenjiahai
* @date 17/9/25
*/
@RestController
@RequestMapping("api")
public class SmsController {
@Autowired
private SmsService smsService;
@Autowired
private TemplateService templateService;
@PostMapping("v3")
public ResponseData sendSmsV3(@RequestBody @Valid SmsMessageCondition condition) throws Exception {
smsService.send(condition);
return ResponseData.build();
}
@GetMapping("v1/templates")
public ResponseData smsTemplateList() {
return ResponseData.builder()
.data("templates", templateService.smsTemplateList())
.build();
}
@PostMapping("v1/templates")
public ResponseData insertTemplate(@RequestBody @Valid InsertTemplateCondition insertTemplateCondition) throws Exception {
return ResponseData.builder()
.data("templateId", templateService.insertTemplate(insertTemplateCondition))
.build();
}
@GetMapping("v1/sms")
public ResponseData sendSms(SmsV1Condition smsCondition) throws SmsException, IOException {
smsService.sendSms(smsCondition);
return ResponseData.build();
}
}
package com.qkdata.sms.api;
import com.qkdata.sms.model.LmobileNotifyCondition;
import com.qkdata.sms.model.SmsReport;
import com.qkdata.sms.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author chenjiahai
* @date 17/9/25
*/
@RestController
@RequestMapping
public class SmsNotifyController {
@Autowired
private SmsService smsService;
@PostMapping("lmobile-notify")
@ResponseStatus(HttpStatus.OK)
public void receive(LmobileNotifyCondition lmobileNotifyCondition) {
smsService.lmobileNotify(lmobileNotifyCondition);
}
@PostMapping("ali-notify")
public ResponseEntity receive(@RequestBody List<SmsReport> smsReports) throws Exception {
smsService.aliNotify(smsReports);
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 0);
jsonObject.put("msg", "成功");
return ResponseEntity.ok(jsonObject.toString());
}
}
package com.qkdata.sms.config;
import com.fasterxml.jackson.annotation.JsonValue;
import java.util.HashMap;
import java.util.Map;
/**
* 所有Entity中枚举类型的基础, 主要用于数据库中存储枚举的value和SpringMVC参数直接转换成Enum
*
* @author chenjiahai
* @date 17/8/3
*/
public interface CommonEntityEnum {
/**
* JsonValue用于serializer
*
* @return
*/
@JsonValue
int value();
/**
* 获取枚举值对应的枚举
*
* @param enumClass 枚举类
* @param enumValue 枚举值
* @return 枚举
*/
static <E extends CommonEntityEnum> E getEnum(final Class<E> enumClass, final Integer enumValue) {
if (enumValue == null) {
return null;
}
try {
return valueOf(enumClass, enumValue);
} catch (final IllegalArgumentException ex) {
return null;
}
}
/**
* 获取枚举值对应的枚举
*
* @param enumClass 枚举类
* @param enumValue 枚举值
* @return 枚举
*/
static <E extends CommonEntityEnum> E valueOf(Class<E> enumClass, Integer enumValue) {
if (enumValue == null) {
throw new NullPointerException("EnumValue is null");
}
return getEnumMap(enumClass).get(enumValue);
}
/**
* 获取枚举键值对
*
* @param enumClass 枚举类型
* @return 键值对
*/
static <E extends CommonEntityEnum> Map<Integer, E> getEnumMap(Class<E> enumClass) {
E[] enums = enumClass.getEnumConstants();
if (enums == null) {
throw new IllegalArgumentException(enumClass.getSimpleName() + " does not represent an enum type.");
}
Map<Integer, E> map = new HashMap<>();
for (E t : enums) {
map.put(t.value(), t);
}
return map;
}
}
package com.qkdata.sms.config;
import com.qkdata.sms.constant.SmsTypeEnum;
import com.qkdata.sms.constant.SmsChannelEnum;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author chenjiahai
*/ // org.apache.ibatis.type.TypeHandlerRegistry#register(TypeHandler<T> typeHandler)#Line:292
@MappedTypes({
SmsChannelEnum.class,
SmsTypeEnum.class
})
@SuppressWarnings("unused")
public class CustomEnumTypeHandler<E extends CommonEntityEnum> extends BaseTypeHandler<E> {
private Class<E> type;
public CustomEnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
E[] enums = type.getEnumConstants();
if (enums == null) {
throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.value());
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
int i = rs.getInt(columnName);
if (rs.wasNull()) {
return null;
} else {
try {
return CommonEntityEnum.getEnum(type, i);
} catch (Exception ex) {
throw new IllegalArgumentException("Cannot convert " + i + " to " + type.getSimpleName() + " by int value.", ex);
}
}
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int i = rs.getInt(columnIndex);
if (rs.wasNull()) {
return null;
} else {
try {
return CommonEntityEnum.getEnum(type, i);
} catch (Exception ex) {
throw new IllegalArgumentException("Cannot convert " + i + " to " + type.getSimpleName() + " by int value.", ex);
}
}
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int i = cs.getInt(columnIndex);
if (cs.wasNull()) {
return null;
} else {
try {
return CommonEntityEnum.getEnum(type, i);
} catch (Exception ex) {
throw new IllegalArgumentException("Cannot convert " + i + " to " + type.getSimpleName() + " by int value.", ex);
}
}
}
}
\ No newline at end of file
package com.qkdata.sms.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfiguration {
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private String timeout;
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(host, port);
configuration.setDatabase(database);
configuration.setPassword(password);
JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
.connectTimeout(Duration.ofSeconds(3))
.usePooling()
.build();
return new JedisConnectionFactory(configuration, clientConfig);
}
// @Bean
// public GenericObjectPoolConfig genericObjectPoolConfig() {
// GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
// genericObjectPoolConfig.setMaxIdle(maxIdle);
// genericObjectPoolConfig.setMinIdle(minIdle);
// genericObjectPoolConfig.setMaxTotal(maxActive);
// genericObjectPoolConfig.setMaxWaitMillis(maxWait.toMillis());
// return genericObjectPoolConfig;
// }
@Bean
public StringRedisTemplate stringRedisTemplate() {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(jedisConnectionFactory());
return stringRedisTemplate;
}
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(jedisConnectionFactory());
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
package com.qkdata.sms.constant;
import java.io.Serializable;
/**
* API 所有ResponseEnum基础
*
* @author chenjiahai
* @date 17/5/14
*/
public interface CommonResponseEnum extends Serializable {
Integer value();
String text();
}
package com.qkdata.sms.constant;
/**
* @author : songminghui
* @date : 17/9/4 上午11:53
* @description :
* @email : songminghui@shangweiec.com
*/
public enum ConstantResponseEnum implements CommonResponseEnum {
OK(0, "ok"),
INTERNAL_ERROR(-1, "服务器异常"),
JSON_PARSE_ERROR(45000, "不合法的请求JSON格式"),
MISSING_PATH_VARIABLE(45001, "缺少请求URL路径参数"),
MISSING_REQUEST_PARAMETER(45002, "缺少请求query参数"),
METHOD_ARGUMENT_TYPE_MIS_MATCH(45003, "不合法的请求参数类型"),
CONSTRAINT_VIOLATION(45004, "请求参数验证未通过"),
MEDIA_TYPE_NOT_SUPPORTED(45005, "不合法的请求Content-Type"),
REQUEST_METHOD_NOT_SUPPORTED(45006, "不合法的请求方法"),
NO_HANDLER_FOUND(45007, "请求资源不存在"),
TEMPLATE_CODE_ISEXIST(45008, "短信模板code已存在");
private Integer value;
private String text;
@Override
public Integer value() {
return value;
}
@Override
public String text() {
return text;
}
ConstantResponseEnum(Integer value, String text) {
this.value = value;
this.text = text;
}
}
package com.qkdata.sms.constant;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* RESTFUL接口统一返回
*
* @author chenjiahai
* @date 17/5/9
*/
@Data
@NoArgsConstructor
public class ResponseData implements Serializable {
private static final long serialVersionUID = -3856663982622822873L;
/**
* 业务编码
*/
private Integer code = ConstantResponseEnum.OK.value();
/**
* 对用户友好的提示信息, 供前端显示使用
*/
private String message = ConstantResponseEnum.OK.text();
/**
* 业务数据
*/
private Map<String, Object> data = new LinkedHashMap<>();
/**
* 不合法参数
*/
private List<Object> errors;
@JsonIgnore
private CommonResponseEnum responseEnum;
public static ResponseDataBuilder builder() {
return new ResponseDataBuilder(new ResponseData());
}
/**
* 默认ResponseData: code为0, message为OK
*
* @return
*/
public static ResponseData build() {
return builder().build();
}
public ResponseData(CommonResponseEnum responseEnum) {
this.code = responseEnum.value();
this.message = responseEnum.text();
}
public ResponseData(HttpStatus httpStatus, String message) {
this.code = httpStatus.value();
this.message = message;
}
public ResponseData(HttpStatus httpStatus, String message, List<Object> errors) {
this(httpStatus, message);
this.errors = errors;
}
}
package com.qkdata.sms.constant;
import java.util.List;
/**
*
* @author chenjiahai
* @date 17/5/19
*/
public class ResponseDataBuilder {
private ResponseData responseData;
public ResponseDataBuilder(ResponseData responseData) {
this.responseData = responseData;
}
public ResponseDataBuilder code(Integer code) {
responseData.setCode(code);
return this;
}
public ResponseDataBuilder message(String userMessage) {
responseData.setMessage(userMessage);
return this;
}
public ResponseDataBuilder result(CommonResponseEnum responseEnum) {
responseData.setCode(responseEnum.value());
responseData.setMessage(responseEnum.text());
return this;
}
public ResponseDataBuilder data(String key, Object value) {
responseData.getData().put(key, value);
return this;
}
public ResponseDataBuilder errors(List<Object> errors) {
responseData.setErrors(errors);
return this;
}
public ResponseData build() {
return responseData;
}
}
package com.qkdata.sms.constant;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
*
* @author chenjiahai
* @date 17/10/9
*/
@Configuration
public class RestTemplateConfiguration {
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
manager.setMaxTotal(20);
return manager;
}
@Bean
public RequestConfig requestConfig() {
return RequestConfig.custom()
.setConnectionRequestTimeout(2000)
.setConnectTimeout(2000)
.setSocketTimeout(2000)
.build();
}
@Bean
public CloseableHttpClient httpClient(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager, RequestConfig requestConfig) {
return HttpClientBuilder
.create()
.setConnectionManager(poolingHttpClientConnectionManager)
.setDefaultRequestConfig(requestConfig)
.build();
}
@Bean
public RestTemplate restTemplate(HttpClient httpClient) {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
return new RestTemplate(requestFactory);
}
}
package com.qkdata.sms.constant;
import com.qkdata.sms.config.CommonEntityEnum;
public enum SmsChannelEnum implements CommonEntityEnum {
ALIBABA(1, "阿里短信平台"),
LMOBILE(2, "乐信通短信平台");
private Integer value;
private String text;
SmsChannelEnum(Integer value, String text) {
this.value = value;
this.text = text;
}
@Override
public int value() {
return this.value;
}
}
package com.qkdata.sms.constant;
/**
* @author chenjiahai
* @date 17/9/27
*/
public enum SmsResponseEnum implements CommonResponseEnum {
SEND_ERROR(92000, "发送短信失败"),
EMPTY_CAPTCHA(92001, "验证码不能为空"),
INVALID_CAPTCHA(92002, "不合法的验证码"),
TEMPLATE_CODE_ISEXIST(92003, "短信模板code已存在"),
TEMPLATE_NOT_EXIST(92004, "短信模板不存在"),
TEMPLATE_PARAMS_MISMATCHED(92005, "模板参数与模板变量不符"),
TEMPLATE_ERROR(92006, "短信模板错误");
private Integer value;
private String text;
SmsResponseEnum(Integer value, String text) {
this.value = value;
this.text = text;
}
@Override
public Integer value() {
return value;
}
@Override
public String text() {
return text;
}
}
package com.qkdata.sms.constant;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
/**
* 短信模板实体
*/
@Data
@NoArgsConstructor
@Entity
@Table(name = "sms_template")
public class SmsTemplate {
/**
* 主键
*/
@Id
@Column(name = "id")
@GeneratedValue(generator = "JDBC")
private Integer id;
/**
* 模板编号,自定义
*/
@Column(name = "code")
private String code;
/**
* 模板类型, 验证码类、通知类、交互类等
*/
@Column(name = "type")
private SmsTypeEnum type;
/**
* 优先使用渠道平台, 阿里短信平台、乐信通短信平台等
*/
@Column(name = "channel")
private SmsChannelEnum channel;
/**
* 模板状态
*/
@Column(name = "status")
private Integer status;
/**
* 阿里模板编号
*/
@Column(name = "alibaba_template_code")
private String alibabaTemplateCode;
/**
* 阿里模板内容
*/
@Column(name = "alibaba_template_content")
private String alibabaTemplateContent;
/**
* 阿里短信签名
*/
@Column(name = "alibaba_sign_name")
private String alibabaSignName;
/**
* 乐信通模板内容
*/
@Column(name = "lmobile_template_content")
private String lmobileTemplateContent;
/**
* 乐信通短信签名
*/
@Column(name = "lmobile_sign_name")
private String lmobileSignName;
/**
* 创建时间
*/
@Column(name = "create_at")
private LocalDateTime createAt;
/**
* 更新时间
*/
@Column(name = "update_at")
private LocalDateTime updateAt;
public SmsTemplate(String code) {
this.code = code;
}
}
package com.qkdata.sms.constant;
import com.qkdata.sms.config.CommonEntityEnum;
/**
* 短信类型枚举
*/
public enum SmsTypeEnum implements CommonEntityEnum {
CAPTCHA(1, "验证码类等"),
NOTIFICATION(2, "通知类等");
private Integer value;
private String text;
SmsTypeEnum(Integer value, String text) {
this.value = value;
this.text = text;
}
@Override
public int value() {
return this.value;
}
}
package com.qkdata.sms.consumer;
public class QueueConstants {
// 验证码短信队列
public static final String CAPTCHA_QUEUE_NAME = "captcha:queue";
// 通知短信队列
public static final String NOTIFICATION_QUEUE_NAME = "notification:queue";
}
package com.qkdata.sms.consumer;
import com.qkdata.sms.exception.SmsException;
import com.qkdata.sms.exception.SmsRemoteApiException;
import com.qkdata.sms.model.SmsCondition;
import com.qkdata.sms.service.SmsSender;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
@Slf4j
public class SendSmsTask implements Callable<Boolean> {
private SmsCondition smsCondition;
private SmsSender aliSmsSender;
private SmsSender lmobileSmsSender;
private SmsSender current;
public SendSmsTask(SmsCondition smsCondition, SmsSender aliSmsSender, SmsSender lmobileSmsSender) {
this.smsCondition = smsCondition;
this.aliSmsSender = aliSmsSender;
this.lmobileSmsSender = lmobileSmsSender;
}
@Override
public Boolean call() throws SmsException {
try {
switchSmsSender();
log.info("即将使用短信平台: {}, 发送短信: {}", this.current.getClass().getSimpleName(), this.smsCondition);
current.send(smsCondition);
} catch (SmsException e) {
throw e;
} catch (SmsRemoteApiException e) {
log.error("调用短信平台接口失败", e);
return false;
}
return true;
}
private void switchSmsSender() {
Integer smsChannel = this.smsCondition.getChannel();
if (this.current == null && smsChannel == 1) {
this.current = aliSmsSender;
} else if (this.current == null && smsChannel == 2) {
this.current = lmobileSmsSender;
} else if (this.current != null && this.current == aliSmsSender) {
this.current = lmobileSmsSender;
} else if (this.current != null && this.current == lmobileSmsSender) {
this.current = aliSmsSender;
} else {
this.current = lmobileSmsSender;
}
}
public SmsCondition getSmsCondition(){
return this.smsCondition;
}
}
package com.qkdata.sms.consumer;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SmsRetryListener implements RetryListener {
@Override
public <Boolean> void onRetry(Attempt<Boolean> attempt) {
log.info("第{}次重试, 距离第一次重试的延迟: {}", attempt.getAttemptNumber(), attempt.getDelaySinceFirstAttempt());
if (attempt.hasException()) {
log.error("重试由于异常终止", attempt.getExceptionCause());
} else {
log.info("重试返回结果: {}", attempt.getResult());
}
}
}
package com.qkdata.sms.consumer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.rholder.retry.*;
import com.google.common.base.Predicates;
import com.qkdata.sms.service.RedisService;
import com.qkdata.sms.exception.SmsRemoteApiException;
import com.qkdata.sms.model.SmsCondition;
import com.qkdata.sms.service.SmsSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Component
@DependsOn
@Slf4j
public class TaskConsumer {
@Autowired
private RedisConnectionFactory jedisConnectionFactory;
@Autowired
@Qualifier("aliSmsSender")
private SmsSender aliSmsSender;
@Autowired
@Qualifier("lmobileSmsSender")
private SmsSender lmobileSmsSender;
@Value("${retry.count}")
private Integer retryCount;
@Autowired
private RedisService redisService;
private static final String ERROR_LIST = "sms:error";
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(2,
Runtime.getRuntime().availableProcessors() * 2, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
@PostConstruct
public void init() {
THREAD_POOL_EXECUTOR.execute(() -> {
RedisConnection connection = jedisConnectionFactory.getConnection();
ObjectMapper objectMapper = new ObjectMapper();
Retryer<Boolean> retryer = buildRetryer();
while (true) {
List<byte[]> bytes = connection.listCommands()
.bLPop(0, QueueConstants.CAPTCHA_QUEUE_NAME.getBytes(StandardCharsets.UTF_8),
QueueConstants.NOTIFICATION_QUEUE_NAME.getBytes(StandardCharsets.UTF_8));
bytes.forEach(task -> {
String value = new String(task);
// 返回结果中第一个元素为队列名称本身, 第二个元素是短信内容, 故忽略
if (QueueConstants.CAPTCHA_QUEUE_NAME.equals(value) || QueueConstants.NOTIFICATION_QUEUE_NAME
.equals(value)) {
return;
}
SmsCondition smsCondition = deserializeSms(objectMapper, value);
if (smsCondition == null) {
return;
}
retry(retryer, new SendSmsTask(smsCondition, aliSmsSender, lmobileSmsSender));
});
}
});
}
private SmsCondition deserializeSms(ObjectMapper objectMapper, String sms) {
SmsCondition smsCondition;
try {
smsCondition = objectMapper.readValue(sms, SmsCondition.class);
} catch (IOException e) {
log.error("反序列化短信参数失败", e);
return null;
}
return smsCondition;
}
private Retryer<Boolean> buildRetryer() {
return RetryerBuilder.<Boolean>newBuilder().retryIfExceptionOfType(SmsRemoteApiException.class)
.retryIfResult(Predicates.equalTo(false))
// .withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
.withWaitStrategy(WaitStrategies.exponentialWait(1000, 60, TimeUnit.SECONDS))
// .withWaitStrategy(WaitStrategies.fibonacciWait(1000, 10, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(retryCount)).withRetryListener(new SmsRetryListener())
.build();
}
private void retry(Retryer<Boolean> retryer, SendSmsTask task) {
try {
// TODO 重试Listener; 重试失败后, 放入另外队列中
retryer.call(task);
} catch (ExecutionException | RetryException e) {
log.error("重试发送失败", e);
String smsCondition = objectToJsonStirng(task.getSmsCondition());
redisService.rpush(ERROR_LIST, smsCondition);
}
}
private String objectToJsonStirng(Object object) {
ObjectMapper objectMapper = new ObjectMapper();
String result = "";
try {
result = objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
log.error("smsCondition序列化失败", e);
}
return result;
}
}
package com.qkdata.sms.exception;
import com.qkdata.sms.constant.CommonResponseEnum;
/**
* 业务异常基类
*
* @author chenjiahai
* @date 17/5/11
*/
public class BusinessException extends Exception {
private static final long serialVersionUID = 1506455046341195098L;
protected final CommonResponseEnum responseEnum;
public BusinessException(CommonResponseEnum responseEnum) {
this.responseEnum = responseEnum;
}
public BusinessException(String message, CommonResponseEnum responseEnum) {
super(message);
this.responseEnum = responseEnum;
}
public CommonResponseEnum responseEnum() {
return responseEnum;
}
}
package com.qkdata.sms.exception;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import com.qkdata.sms.constant.ConstantResponseEnum;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.qkdata.sms.constant.ResponseData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
/**
* 业务异常处理类
* @author wangcy
*
*/
@RestControllerAdvice
@Slf4j
public class ControllerExceptionHandleAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<?> bindExceptionHandler(HttpServletRequest req, HttpServletResponse res, MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String message = bindingResult.getFieldError().getDefaultMessage();
log.info(message);
return ResponseEntity.badRequest().body(new ResponseData(HttpStatus.BAD_REQUEST, message));
}
@ExceptionHandler(value = BusinessException.class)
public ResponseEntity<?> businessExceptionHandler(HttpServletRequest req, HttpServletResponse res, BusinessException e) {
String message = e.getMessage();
log.info(message);
return ResponseEntity.badRequest().body(new ResponseData(HttpStatus.INTERNAL_SERVER_ERROR, message));
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<?> httpMessageConvertExceptionHanlder(HttpMessageNotReadableException e) {
return responseData(ConstantResponseEnum.JSON_PARSE_ERROR, e);
}
@ExceptionHandler(MissingPathVariableException.class)
public ResponseEntity<?> missingPathVariableExceptionHanlder(MissingPathVariableException e) {
return responseData(ConstantResponseEnum.MISSING_PATH_VARIABLE, e);
}
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<?> requestParameterExceptionHandler(MissingServletRequestParameterException e) {
return responseData(ConstantResponseEnum.MISSING_REQUEST_PARAMETER, e);
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException e) {
return responseData(ConstantResponseEnum.METHOD_ARGUMENT_TYPE_MIS_MATCH, e);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<?> constraintViolationExceptionHandler(ConstraintViolationException e) {
log(e);
return ResponseEntity.badRequest().body(ResponseData.builder()
.code(ConstantResponseEnum.CONSTRAINT_VIOLATION.value())
.message(e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).findFirst().get())
.build());
}
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<?> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException e) {
return responseData(ConstantResponseEnum.MEDIA_TYPE_NOT_SUPPORTED, e);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) {
return responseData(ConstantResponseEnum.REQUEST_METHOD_NOT_SUPPORTED, e);
}
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<?> noHandlerFoundExceptionHandler(NoHandlerFoundException e) {
return responseData(ConstantResponseEnum.NO_HANDLER_FOUND, e);
}
private void log(Throwable e) {
log.error(e.getMessage(), e);
}
private ResponseEntity<?> responseData(ConstantResponseEnum responseEnum, Throwable e) {
log(e);
return ResponseEntity.badRequest().body(ResponseData.builder()
.result(responseEnum)
.build());
}
}
package com.qkdata.sms.exception;
import com.qkdata.sms.constant.CommonResponseEnum;
/**
*
* @author chenjiahai
* @date 17/9/27
*/
public class SmsException extends BusinessException {
private static final long serialVersionUID = -6404413103759832403L;
public SmsException(String message, CommonResponseEnum responseEnum) {
super(message, responseEnum);
}
}
package com.qkdata.sms.exception;
import com.qkdata.sms.constant.CommonResponseEnum;
public class SmsRemoteApiException extends BusinessException {
public SmsRemoteApiException(CommonResponseEnum responseEnum) {
super(responseEnum);
}
public SmsRemoteApiException(String message, CommonResponseEnum responseEnum) {
super(message, responseEnum);
}
}
package com.qkdata.sms.lmobile;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 个性化短信请求参数
*/
@Data
public class IndividuationSmsRequest implements Serializable {
private static final long serialVersionUID = -1838385964799608368L;
/**
* 提交账户, 必传
*/
@JsonProperty("User")
private String user;
/**
* 提交账户密码, 必传
*/
@JsonProperty("Password")
private String password;
/**
* 企业代码, 非必传
*/
@JsonProperty("CorpId")
private String corpId;
/**
* 产品编号, 必传
*/
@JsonProperty("ProductId")
private String productId;
/**
* 短信变量, 必传
*/
@JsonProperty("SmsVariable")
private String smsVariable;
/**
* 短信模板, 必传
*/
@JsonProperty("SmsTemplate")
private String smsTemplate;
/**
* 用户自定义参数,长度<=32, 非必传
*/
@JsonProperty("key")
private String key;
}
package com.qkdata.sms.lmobile;
import lombok.Data;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
@XmlRootElement(name = "ISMV")
@XmlAccessorType(XmlAccessType.FIELD)
@Data
public class IndividuationSmsVariableRequest {
@XmlElement(name = "VU")
private List<MobileUnit> mobileUnits;
}
package com.qkdata.sms.lmobile;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import java.util.List;
/**
* 号码单元, 一个手机号对应一个单元, 多个则对应多个
*/
@Data
@NoArgsConstructor
@XmlAccessorType(XmlAccessType.FIELD)
public class MobileUnit {
@XmlElement(name = "VT")
private List<MobileUnitValue> mobileUnitValues;
}
\ No newline at end of file
package com.qkdata.sms.lmobile;
import lombok.Data;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public class MobileUnitValue {
@XmlElement(name = "V")
private String value;
}
\ No newline at end of file
package com.qkdata.sms.lmobile;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 用于提交发送短信的常规方法
*/
@Data
public class RegularSmsRequest implements Serializable {
private static final long serialVersionUID = -5139319999792779765L;
private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 提交账户,必传
*/
@JsonProperty("sname")
private String user;
/**
* 提交账户的密码,必传
*/
@JsonProperty("spwd")
private String password;
/**
* 企业代码,非必传
*/
@JsonProperty("scorpid")
private String corpId;
/**
* 产品编号,必传
*/
@JsonProperty("sprdid")
private String productId;
/**
* 接收号码间用英文半角逗号","隔开,触发产品一次只能提交一个
* 其他产品一次不能超过10万个号码
*/
@JsonProperty("sdst")
private String mobiles;
/**
* 短信内容
*/
@JsonProperty("smsg")
private String content;
/**
* 短信定时时间,格式:yyyy-MM-dd HH:mm:ss,要求大于当前时间并且小于当前时间+31天
*/
private LocalDateTime beginDate;
/**
* 用户自定义参数,长度<=32
*/
@JsonProperty("key")
private String key;
@JsonProperty("sbegindate")
public String getBeginDate() {
if (beginDate == null) {
return null;
}
return beginDate.format(FORMAT);
}
}
package com.qkdata.sms.model;
import com.qkdata.sms.constant.SmsChannelEnum;
import com.qkdata.sms.constant.SmsTypeEnum;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 短信发送统一请求参数
*/
@Data
public class InsertTemplateCondition implements Serializable {
private static final long serialVersionUID = 462051340401877051L;
@NotBlank(message = "模板code不能为空")
private String code;
@NotNull(message = "模板类型不能为空")
private SmsTypeEnum type;
@NotNull(message = "优先使用平台不能为空")
private SmsChannelEnum channel;
@NotBlank(message = "阿里短信code不能为空")
private String alibabaTemplateCode;
@NotBlank(message = "阿里短信模板不能为空")
private String alibabaTemplateContent;
@NotBlank(message = "阿里短信签名不能为空")
private String alibabaSignName;
@NotBlank(message = "乐信通短信模板不能为空")
private String lmobileTemplateContent;
@NotBlank(message = "乐信通短信签名不能为空")
private String lmobileSignName;
}
package com.qkdata.sms.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 个性化短信请求参数
*/
@Data
public class LmobileNotifyCondition implements Serializable {
private static final long serialVersionUID = -1038085064793608368L;
/**
* 客户账号
* 默认传递:是
*/
@JsonProperty("AccountID")
private String accountID;
/**
* 信息编号(同短信提交时产生的MsgID)
* 默认传递:是
*/
@JsonProperty("MsgID")
private String msgID;
/**
* 客户提交手机号码
* 默认传递:是
*/
@JsonProperty("MobilePhone")
private String mobilePhone;
/**
* 状态报告说明(运营商送达手机终端的回执),针对ReportState
* 默认传递:是
*/
@JsonProperty("ReportResultInfo")
private String reportResultInfo;
/**
* 状态报告结果,True是成功,False是失败
* 默认传递:是
*/
@JsonProperty("ReportState")
private Boolean reportState;
/**
* 状态报告时间
* 默认传递:是
*/
@JsonProperty("ReportTime")
private String reportTime;
/**
* 发送结果,针对SendState
* 默认传递:是
*/
@JsonProperty("SendResultInfo")
private String sendResultInfo;
/**
* 发送状态,供应商送达运营商网关的状态,True是成功,False是失败
* 默认传递:是
*/
@JsonProperty("SendState")
private Boolean sendState;
/**
* 短信发送时间
* 默认传递:是
*/
@JsonProperty("SendedTime")
private String sendedTime;
/**
* 长号码(下发端口号)
* 默认传递:是
*/
@JsonProperty("SPNumber")
private String spNumber;
/**
* 自定义参数(可选),与短信提交接口g_SubmitWithKey中的Key一致
* 默认传递:否
*/
@JsonProperty("ClientMsgId")
private String clientMsgId;
/**
* 扩展码
* 默认传递:否
*/
@JsonProperty("ExtendNum")
private String extendNum;
/**
* 长短信序号
* 默认传递:否
*/
@JsonProperty("LongMsgNum")
private String longMsgNum;
/**
* 长短信总条数
* 默认传递:否
*/
@JsonProperty("LongMsgTotal")
private String longMsgTotal;
}
package com.qkdata.sms.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
public class LmobileResponse implements Serializable {
private static final long serialVersionUID = 1776398138887445771L;
@JsonProperty("MsgState")
private String msgState;
@JsonProperty("State")
private Integer state;
@JsonProperty("MsgID")
private String msgId;
@JsonProperty("Reserve")
private Integer reserve;
}
package com.qkdata.sms.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.format.DateTimeFormatter;
/**
* 用于提交发送短信的常规方法
*/
@Data
@NoArgsConstructor
public class NotifyRequestDTO implements Serializable {
private static final long serialVersionUID = -5139715979792739765L;
private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String sendTime;
private String reportTime;
private String errCode;
private String errMsg;
private String outId;
}
package com.qkdata.sms.model;
import java.io.Serializable;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsAliCondition implements Serializable {
/**
*
*/
private static final long serialVersionUID = 8199548590381624271L;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3,4,5,6,7,8,9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
@NotBlank(message = "签名不能为空")
private String signName;
@NotBlank(message = "模板编号不能为空")
private String templateCode;
@NotBlank(message = "模板参数不能为空")
private String templateParam;
@NotBlank(message = "outId不能为空")
private String outId;
}
package com.qkdata.sms.model;
import lombok.Data;
import java.io.Serializable;
@Data
public class SmsApiV1Condition implements Serializable {
private static final long serialVersionUID = -6534578898552563728L;
private final String type = "send";
private String username;
private String password_md5;
private String apikey;
private final String encode = "UTF-8";
private String mobile;
private String content;
}
package com.qkdata.sms.model;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class SmsApiV1Response {
public static final Map<String, String> RESPONSE_RESULT = new HashMap<>();
public static final String SUCCESS_RESPONSE_KEY = "success";
static {
RESPONSE_RESULT.put("Missing username", "用户名为空");
RESPONSE_RESULT.put("Missing password", "密码为空");
RESPONSE_RESULT.put("Missing apikey", "APIKEY为空");
RESPONSE_RESULT.put("Missing recipient", "收件人手机号码为空");
RESPONSE_RESULT.put("Missing message content", "短信内容为空或编码不正确");
RESPONSE_RESULT.put("Account is blocked", "账号被禁用");
RESPONSE_RESULT.put("Unrecognized encoding", "编码未能识别");
RESPONSE_RESULT.put("APIKEY or password error", "APIKEY 或密码错误");
RESPONSE_RESULT.put("Unauthorized IP address", "未授权IP地址");
RESPONSE_RESULT.put("Account balance is insufficient", "余额不足");
RESPONSE_RESULT.put("Throughput Rate Exceeded", "发送频率受限");
RESPONSE_RESULT.put("Invalid md5 password length", "MD5 密码长度非32位");
}
private String status;
private String msg;
}
package com.qkdata.sms.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.Map;
@Data
@NoArgsConstructor
public class SmsCondition {
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3,4,5,6,7,8,9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
/**
* 模板编号
*/
@NotBlank(message = "模板编号不能为空")
private String code;
/**
* 模板参数, JSON格式
*/
private Map<String, Object> params;
private String outId;
private String notifyUrl;
private Integer channel;
}
package com.qkdata.sms.model;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.util.List;
/**
* 短信发送统一请求参数
*/
@Data
public class SmsMessageCondition implements Serializable {
private static final long serialVersionUID = 462051340401877051L;
@NotEmpty(message = "短信参数不能为空列表")
private List<SmsCondition> conditions;
}
package com.qkdata.sms.model;
import lombok.Data;
@Data
public class SmsReport {
private String phone_number;
private Boolean success;
private String biz_id;
private String out_id;
private String send_time;
private String report_time;
private String err_code;
private String err_msg;
private String sms_size;
}
package com.qkdata.sms.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.format.DateTimeFormatter;
/**
* 用于提交发送短信的常规方法
*/
@Data
@NoArgsConstructor
public class SmsTypeAndChannelDTO implements Serializable {
private static final long serialVersionUID = -1651569621968817053L;
private Integer type;
private Integer channel;
}
package com.qkdata.sms.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsV1Condition {
private static final long serialVersionUID = -7094472942961784909L;
@NotBlank(message = "mobile不能为空")
@Pattern(regexp = "^1[3,4,5,6,7,8,9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
private String messageContent;
}
package com.qkdata.sms.model;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "sms")
@Data
public class SmsV1Property {
private String username;
private String password;
private String apikey;
private String url;
private String template;
}
package com.qkdata.sms.notification;
import com.qkdata.sms.model.NotifyRequestDTO;
public abstract class AbstractNotifyRequest implements NotifyRequest {
protected NotifyRequestDTO notifyRequestDTO;
public AbstractNotifyRequest() {
this.notifyRequestDTO = new NotifyRequestDTO();
// init();
}
protected void init() {
setSendTime();
setReportTime();
setErrCode();
setErrMsg();
setOutId();
}
public final NotifyRequestDTO notifyRequest() {
return this.notifyRequestDTO;
}
}
package com.qkdata.sms.notification;
import com.qkdata.sms.model.SmsReport;
public class AliNotifyRequest extends AbstractNotifyRequest {
private SmsReport smsReport;
public AliNotifyRequest(SmsReport smsReport) {
super();
this.smsReport = smsReport;
super.init();
}
@Override
public void setSendTime() {
super.notifyRequestDTO.setSendTime(smsReport.getSend_time());
}
@Override
public void setReportTime() {
super.notifyRequestDTO.setReportTime(smsReport.getReport_time());
}
@Override
public void setErrCode() {
if (!"DELIVERED".equals(smsReport.getErr_code())) {
super.notifyRequestDTO.setErrCode("UNDELIVERD");
} else {
super.notifyRequestDTO.setErrCode(smsReport.getErr_code());
}
}
@Override
public void setErrMsg() {
super.notifyRequestDTO.setErrMsg(smsReport.getErr_msg());
}
@Override
public void setOutId() {
super.notifyRequestDTO.setOutId(smsReport.getOut_id());
}
}
package com.qkdata.sms.notification;
import com.qkdata.sms.model.LmobileNotifyCondition;
public class LmobileNotifyRequest extends AbstractNotifyRequest {
private LmobileNotifyCondition condition;
public LmobileNotifyRequest(LmobileNotifyCondition condition) {
super();
this.condition = condition;
super.init();
}
@Override
public void setSendTime() {
super.notifyRequestDTO.setSendTime(condition.getSendedTime());
}
@Override
public void setReportTime() {
super.notifyRequestDTO.setReportTime(condition.getReportTime());
}
@Override
public void setErrCode() {
if (condition.getReportState()) {
super.notifyRequestDTO.setErrCode("DELIVERED");
} else {
super.notifyRequestDTO.setErrCode("UNDELIVERD");
}
}
@Override
public void setErrMsg() {
super.notifyRequestDTO.setErrMsg(condition.getSendResultInfo());
}
@Override
public void setOutId() {
super.notifyRequestDTO.setOutId(condition.getClientMsgId());
}
}
package com.qkdata.sms.notification;
import com.qkdata.sms.model.NotifyRequestDTO;
public interface NotifyRequest {
void setSendTime();
void setReportTime();
void setErrCode();
void setErrMsg();
void setOutId();
NotifyRequestDTO notifyRequest();
}
package com.qkdata.sms.notification;
import com.aliyuncs.utils.StringUtils;
import com.qkdata.sms.model.NotifyRequestDTO;
import com.qkdata.sms.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
@Slf4j
public class NotifyTemplate {
@Autowired
private RedisService redisService;
@Autowired
private RestTemplate restTemplate;
/**
* 将短信报告存储到Redis中
*
* @param key
* @param report
*/
private void pushReportToRedis(String key, String report) {
redisService.rpush(key, report);
}
/**
* 实际回调
*
* @param notifyUrl
* @param notifyRequest
*/
private void request(String notifyUrl, NotifyRequest notifyRequest) {
if (notifyRequest == null) {
return;
}
NotifyRequestDTO notifyRequestDTO = notifyRequest.notifyRequest();
if (notifyRequestDTO == null) {
return;
}
// TODO 是否outId必须才回调
if (StringUtils.isEmpty(notifyRequestDTO.getOutId())) {
return;
}
String rep;
try {
rep = restTemplate.postForObject(notifyUrl, notifyRequestDTO, String.class);
log.info("回调:{},返回信息:{}", notifyUrl, rep);
} catch (Exception e) {
log.error("回调:{}失败", notifyUrl, e);
}
// TODO 是否需要加入重试
}
/**
* 从Redis中获取回调地址
*
* @param key
* @return
*/
public String getNotifyUrl(String key) {
return (String) redisService.get(key);
}
/**
* 删除Redis中回调地址
*
* @param key
*/
private void deleteNotificationUrlFromRedis(String key) {
redisService.delete(key);
}
/**
* 回调通知流程
*
* @param pushKey 存储短信报告Key
* @param report 短信报告JSON字符串
* @param notifyUrlKey 回调地址Key
* @param notifyRequest 回调请求参数
*/
public void notify(String pushKey, String report, String notifyUrl, String notifyUrlKey, NotifyRequest notifyRequest) {
request(notifyUrl, notifyRequest);
pushReportToRedis(pushKey, report);
deleteNotificationUrlFromRedis(notifyUrlKey);
}
}
package com.qkdata.sms.repository;
import com.qkdata.sms.constant.SmsTemplate;
import com.qkdata.sms.model.SmsTypeAndChannelDTO;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
import java.util.List;
public interface SmsTemplateMapper extends Mapper<SmsTemplate> {
Integer countTemplateByCode(@Param("codeList")List<String> codeList);
SmsTypeAndChannelDTO getTypeAndChannelByCode(@Param("code") String code);
}
package com.qkdata.sms.service;
import com.qkdata.sms.exception.SmsException;
import com.qkdata.sms.constant.SmsResponseEnum;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class AbstractPlaceholderResolver implements PlaceholderResolver {
protected abstract String placeholderKeyword();
protected abstract Pattern pattern();
/**
* 1. 模板中是否包含占位符, 如果不包含直接返回true
* 2. 如果包含, 判断params是否为空, 如果为空,抛出异常
* 3. 如果params不为空, 解析template,并且判断params与placeholder是否完全匹配,忽略掉params中多余的参数,如果完全匹配,返回true
* @param template
* @param params
* @return
*/
@Override
public boolean isMatch(String template, Map<String, Object> params) throws SmsException {
if (!hasPlaceholder(template, placeholderKeyword())) {
return true;
}
if (params == null || params.size() == 0) {
throw new SmsException("模板中有占位符,但是并未提供参数", SmsResponseEnum.TEMPLATE_PARAMS_MISMATCHED);
}
Matcher matcher = pattern().matcher(template);
while (matcher.find()) {
String keyword = matcher.group(1);
String value = (String) params.get(keyword);
if (StringUtils.isEmpty(value)) {
throw new SmsException(String.format("模板参数与模板占位符不符, 占位符: %s", keyword),
SmsResponseEnum.TEMPLATE_PARAMS_MISMATCHED);
}
}
return true;
}
@Override
public String replace(String template, Map<String, Object> params) throws SmsException {
return template;
}
}
package com.qkdata.sms.service;
import lombok.extern.slf4j.Slf4j;
import java.util.regex.Pattern;
@Slf4j
public class AliPlaceholderResolver extends AbstractPlaceholderResolver {
private static final Pattern PATTERN = Pattern.compile("\\$\\{(.*?)\\}");
private static final String PLACEHOLDER_KEYWORD = "$";
@Override
protected String placeholderKeyword() {
return PLACEHOLDER_KEYWORD;
}
@Override
protected Pattern pattern() {
return PATTERN;
}
}
package com.qkdata.sms.service;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qkdata.sms.constant.SmsTemplate;
import com.qkdata.sms.exception.SmsException;
import com.qkdata.sms.constant.SmsResponseEnum;
import com.qkdata.sms.exception.SmsRemoteApiException;
import com.qkdata.sms.model.SmsCondition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Map;
@Service
@Slf4j
public class AliSmsSender implements SmsSender {
@Autowired
private TemplateService templateService;
@Autowired
private RedisService redisService;
@Value("${aliyun.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.accessKeySecret}")
private String accessKeySecret;
private static final String ALI_TYPE = ":ali";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
public void send(SmsCondition sms) throws SmsException, SmsRemoteApiException {
SmsTemplate smsTemplate = templateService.getTemplateByCode(sms.getCode());
Map<String, Object> paramsMap = sms.getParams();
String content = smsTemplate.getAlibabaTemplateContent();
PlaceholderResolver placeholderResolver = new AliPlaceholderResolver();
if (!placeholderResolver.isMatch(content, paramsMap)) {
return;
}
try {
SendSmsResponse sendSmsResponse = submitSms(sms.getMobile(), smsTemplate.getAlibabaSignName(),
smsTemplate.getAlibabaTemplateCode(), paramsToJson(paramsMap), sms.getOutId(), accessKeyId,
accessKeySecret);
if (sendSmsResponse.getCode() != null && !"OK".equals(sendSmsResponse.getCode())) {
if (sendSmsResponse.getCode().equals("isv.AMOUNT_NOT_ENOUGH")) {
log.error("阿里短信平台余额不足:{},{}", sendSmsResponse.getCode(), sendSmsResponse.getMessage());
} else {
log.error("阿里短信发送失败:{},{}", sendSmsResponse.getCode(), sendSmsResponse.getMessage());
}
throw new SmsRemoteApiException("阿里短信发送失败", SmsResponseEnum.SEND_ERROR);
} else {
log.info("{}---发送成功:{}", sms.getMobile(), content);
if (!StringUtils.isEmpty(sms.getOutId()) && !StringUtils.isEmpty(sms.getNotifyUrl())) {
redisService.set(sms.getOutId() + ALI_TYPE, sms.getNotifyUrl());
}
}
} catch (Exception e) {
log.error("调用阿里云短信平台失败", e);
throw new SmsRemoteApiException("阿里短信发送失败", SmsResponseEnum.SEND_ERROR);
}
}
private String paramsToJson(Map<String, Object> paramsMap) {
String paramsJson = "";
if (paramsMap == null || paramsMap.size() == 0) {
return paramsJson;
}
try {
paramsJson = OBJECT_MAPPER.writeValueAsString(paramsMap);
} catch (JsonProcessingException e) {
// do nothing
}
return paramsJson;
}
private SendSmsResponse submitSms(String mobile, String signName, String templateCode, String templateParams,
String outId, String accessKeyId, String accessKeySecret) throws ClientException {
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// final String product = "Dysmsapi";//短信API产品名称(短信产品名固定,无需修改)
// final String domain = "dysmsapi.aliyuncs.com";//短信API产品域名(接口地址固定,无需修改)
//初始化ascClient,暂时不支持多region(请勿修改)
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
// DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setMethod(MethodType.POST);
//必填:待发送手机号。支持以逗号分隔的形式进行批量调用,批量上限为1000个手机号码,批量调用相对于单条调用及时性稍有延迟,验证码类型的短信推荐使用单条调用的方式;发送国际/港澳台消息时,接收号码格式为00+国际区号+号码,如“0085200000000”
request.setPhoneNumbers(mobile);
//必填:短信签名-可在短信控制台中找到
request.setSignName(signName);
//必填:短信模板-可在短信控制台中找到,发送国际/港澳台消息时,请使用国际/港澳台短信模版
request.setTemplateCode(templateCode);
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
//友情提示:如果JSON中需要带换行符,请参照标准的JSON协议对换行符的要求,比如短信内容中包含\r\n的情况在JSON中需要表示成\\r\\n,否则会导致JSON在服务端解析失败
request.setTemplateParam(templateParams);
//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
if (!StringUtils.isEmpty(outId)) {
request.setOutId(outId);
}
return acsClient.getAcsResponse(request);
}
}
package com.qkdata.sms.service;
import com.qkdata.sms.exception.SmsException;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class LmobilePlaceholderResolver extends AbstractPlaceholderResolver {
private static final Pattern PATTERN = Pattern.compile("#(.*?)#");
private static final String PLACEHOLDER_KEYWORD = "#";
@Override
protected String placeholderKeyword() {
return PLACEHOLDER_KEYWORD;
}
@Override
protected Pattern pattern() {
return PATTERN;
}
@Override
public String replace(String template, Map<String, Object> params) throws SmsException {
String result = template;
if (isMatch(template, params)) {
Matcher matcher = PATTERN.matcher(template);
while (matcher.find()) {
String paramKey = matcher.group(1);
String value = (String) params.get(paramKey);
result = result.replace(matcher.group(0), value);
}
}
return result;
}
}
package com.qkdata.sms.service;
import com.qkdata.sms.constant.SmsTemplate;
import com.qkdata.sms.exception.SmsException;
import com.qkdata.sms.exception.SmsRemoteApiException;
import com.qkdata.sms.lmobile.RegularSmsRequest;
import com.qkdata.sms.model.LmobileResponse;
import com.qkdata.sms.model.SmsCondition;
import com.qkdata.sms.constant.SmsResponseEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Service
@Slf4j
public class LmobileSmsSender implements SmsSender {
@Autowired
private TemplateService templateService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private RedisService redisService;
@Value("${lmobile.user}")
private String user;
@Value("${lmobile.password}")
private String password;
@Value("${lmobile.productId}")
private String productId;
@Value("${lmobile.url}")
private String url;
private static final String LMOBILE_TYPE = ":lmobile";
@Override
public void send(SmsCondition sms) throws SmsException, SmsRemoteApiException {
SmsTemplate smsTemplate = templateService.getTemplateByCode(sms.getCode());
Map<String, Object> paramsMap = sms.getParams();
String content = smsTemplate.getLmobileTemplateContent();
PlaceholderResolver placeholderResolver = new LmobilePlaceholderResolver();
content = placeholderResolver.replace(content, paramsMap);
content = appendSignNameToContent(content, smsTemplate.getLmobileSignName());
RegularSmsRequest regularSmsRequest = new RegularSmsRequest();
regularSmsRequest.setUser(user);
regularSmsRequest.setPassword(password);
regularSmsRequest.setProductId(productId);
regularSmsRequest.setMobiles(sms.getMobile());
if (!StringUtils.isEmpty(sms.getOutId())) {
regularSmsRequest.setKey(sms.getOutId());
}
regularSmsRequest.setContent(content);
try {
LmobileResponse lmobileResponse = restTemplate.postForObject(url, regularSmsRequest, LmobileResponse.class);
if (!lmobileResponse.getState().equals(0)) {
if (lmobileResponse.getState().equals(1025)) {
log.error("Lmobile短信平台余额不足:{}", lmobileResponse.getMsgState());
} else {
log.error("Lmobile短信发送失败:{}", lmobileResponse.getMsgState());
}
throw new SmsRemoteApiException("短信发送失败", SmsResponseEnum.SEND_ERROR);
} else {
log.info("{}---发送成功:{}", sms.getMobile(), content);
if (!StringUtils.isEmpty(sms.getOutId()) && !StringUtils.isEmpty(sms.getNotifyUrl())) {
redisService.set(sms.getOutId() + LMOBILE_TYPE, sms.getNotifyUrl());
}
}
} catch (Exception e) {
log.error("lmobie发送失败", e);
throw new SmsRemoteApiException("短信发送失败", SmsResponseEnum.SEND_ERROR);
}
}
private String appendSignNameToContent(String content, String signName) {
return content + "【" + signName + "】";
}
}
package com.qkdata.sms.service;
import com.qkdata.sms.exception.SmsException;
import org.springframework.util.StringUtils;
import java.util.Map;
public interface PlaceholderResolver {
boolean isMatch(String template, Map<String, Object> params) throws SmsException;
String replace(String template, Map<String, Object> params) throws SmsException;
/**
* 模板内容是否包含占位符,简单的用$判断,所以模板正式内容不能出现此字符
* @param template 模板内容
* @param keyword 占位符关键字, ali为$, lmobile为#
* @return 是否包含
*/
default boolean hasPlaceholder(String template, String keyword) {
if (!StringUtils.isEmpty(template) && template.indexOf(keyword) > 0) {
return true;
}
return false;
}
}
package com.qkdata.sms.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate redisTemplate;
public Long lpush(String queue, List<String> values) {
Objects.requireNonNull(queue, "队列名称不能为空");
if (CollectionUtils.isEmpty(values)) {
throw new IllegalArgumentException("队列值不能为空");
}
return stringRedisTemplate.opsForList().leftPushAll(queue, values);
}
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public void delete(String key) {
redisTemplate.opsForValue().getOperations().delete(key);
}
public void delete(Collection keys) {
redisTemplate.opsForValue().getOperations().delete(keys);
}
public Set keys(String pattern){
return redisTemplate.keys(pattern);
}
public Long rpush(String key, String value){
return redisTemplate.opsForList().rightPush(key, value);
}
public List range(String key, Long start, Long end){
return redisTemplate.opsForList().range(key, start, end);
}
}
package com.qkdata.sms.service;
import com.qkdata.sms.exception.SmsException;
import com.qkdata.sms.exception.SmsRemoteApiException;
import com.qkdata.sms.model.SmsCondition;
public interface SmsSender {
void send(SmsCondition sms) throws SmsException, SmsRemoteApiException;
}
package com.qkdata.sms.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qkdata.sms.consumer.QueueConstants;
import com.qkdata.sms.exception.SmsException;
import com.qkdata.sms.model.*;
import com.qkdata.sms.constant.SmsResponseEnum;
import com.qkdata.sms.model.*;
import com.qkdata.sms.notification.AliNotifyRequest;
import com.qkdata.sms.notification.LmobileNotifyRequest;
import com.qkdata.sms.notification.NotifyRequest;
import com.qkdata.sms.notification.NotifyTemplate;
import com.qkdata.sms.util.RandomDigitGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import javax.validation.Valid;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import static java.util.stream.Collectors.toList;
@Service("smsService")
@Slf4j
@EnableConfigurationProperties(SmsV1Property.class)
public class SmsService {
@Autowired
private RedisService redisService;
@Autowired
private TemplateService templateService;
@Autowired
private NotifyTemplate notifyTemplate;
@Autowired
private RestTemplate restTemplate;
@Autowired
private SmsV1Property smsProperty;
private static final String ALI_SMS_NOTIFY_KEY = ":ali";
private static final String LMOBILE_SMS_NOTIFY_KEY = ":lmobile";
private static final String SMS_NOTIFY_LIST_NAME = "sms:notify";
public void send(SmsMessageCondition condition) throws Exception {
List<SmsCondition> conditions = condition.getConditions();
if (!judgeTemplateCodeIsExist(conditions)) {
throw new SmsException("短信模板不存在", SmsResponseEnum.TEMPLATE_NOT_EXIST);
}
List<SmsCondition> captchaSms = new LinkedList<>();
List<SmsCondition> notificationSms = new LinkedList<>();
for (SmsCondition smsCondition : conditions) {
SmsTypeAndChannelDTO smsTypeAndChannelDTO = templateService.getTypeAndChannelByCode(smsCondition.getCode());
smsCondition.setChannel(smsTypeAndChannelDTO.getChannel());
if (smsTypeAndChannelDTO.getType() == 1) {
captchaSms.add(smsCondition);
}
if (smsTypeAndChannelDTO.getType() == 2) {
notificationSms.add(smsCondition);
}
}
if (!CollectionUtils.isEmpty(captchaSms)) {
redisService.lpush(QueueConstants.CAPTCHA_QUEUE_NAME, jsonFrom(captchaSms));
}
if (!CollectionUtils.isEmpty(notificationSms)) {
redisService.lpush(QueueConstants.NOTIFICATION_QUEUE_NAME, jsonFrom(notificationSms));
}
}
private List<String> jsonFrom(List<SmsCondition> smses) {
if (CollectionUtils.isEmpty(smses)) {
return Collections.emptyList();
}
ObjectMapper objectMapper = new ObjectMapper();
return smses.stream().map(sms -> {
try {
return objectMapper.writeValueAsString(sms);
} catch (JsonProcessingException e) {
// TODO
e.printStackTrace();
}
return null;
}).collect(toList());
}
private Boolean judgeTemplateCodeIsExist(List<SmsCondition> smsConditionList) {
List<String> codeList = new ArrayList<>();
for (SmsCondition smsCondition : smsConditionList) {
if (!codeList.contains(smsCondition.getCode())) {
codeList.add(smsCondition.getCode());
}
}
Integer count = templateService.getTemplateCount(codeList);
if (count.equals(codeList.size())) {
return true;
}
return false;
}
public void aliNotify(List<SmsReport> smsReportList) {
for (SmsReport smsReport : smsReportList) {
log.info("阿里回调参数: {}", smsReport);
if (StringUtils.isEmpty(smsReport.getOut_id())) {
continue;
}
String notifyUrlKey = smsReport.getOut_id() + ALI_SMS_NOTIFY_KEY;
String notifyUrl = notifyTemplate.getNotifyUrl(notifyUrlKey);
if (StringUtils.isEmpty(notifyUrl)) {
continue;
}
String notifyString = objectToJsonStirng(smsReport);
NotifyRequest request = new AliNotifyRequest(smsReport);
notifyTemplate.notify(SMS_NOTIFY_LIST_NAME, notifyString, notifyUrl, notifyUrlKey, request);
}
}
public void lmobileNotify(LmobileNotifyCondition lmobileNotifyCondition) {
log.info("lmobile回调通知参数: {}", lmobileNotifyCondition);
if (StringUtils.isEmpty(lmobileNotifyCondition.getClientMsgId())) {
return;
}
String notifyUrlKey = lmobileNotifyCondition.getClientMsgId() + LMOBILE_SMS_NOTIFY_KEY;
String notifyUrl = notifyTemplate.getNotifyUrl(notifyUrlKey);
if (StringUtils.isEmpty(notifyUrl)) {
return;
}
String notifyString = objectToJsonStirng(lmobileNotifyCondition);
NotifyRequest request = new LmobileNotifyRequest(lmobileNotifyCondition);
notifyTemplate.notify(SMS_NOTIFY_LIST_NAME, notifyString, notifyUrl, notifyUrlKey, request);
}
// TODO 抽取
private String objectToJsonStirng(Object object) {
ObjectMapper objectMapper = new ObjectMapper();
String result = "";
try {
result = objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
log.error("smsCondition序列化失败", e);
}
return result;
}
public void sendSms(@Valid SmsV1Condition smsCondition) throws SmsException, IOException {
log.info("手机号:" + smsCondition.getMobile() + ";内容:" + smsCondition.getMessageContent());
String url = String.join("", smsProperty.getUrl(), "?format=json&data={json}");
String response = restTemplate.getForObject(url, String.class, requestJson(smsCondition.getMobile(), smsCondition.getMessageContent()));
log.info("发送短信响应结果: " + response);
ObjectMapper objectMapper = new ObjectMapper();
SmsApiV1Response smsApiResponse = objectMapper.readValue(response, SmsApiV1Response.class);
if (SmsApiV1Response.SUCCESS_RESPONSE_KEY.equals(smsApiResponse.getStatus())) {
return;
} else {
throw new SmsException(String.format("发送短信失败, 失败原因: %s", SmsApiV1Response.RESPONSE_RESULT.get(smsApiResponse.getMsg())), SmsResponseEnum.SEND_ERROR);
}
}
private String requestJson(String mobile, String messageContent) throws SmsException {
try {
SmsApiV1Condition condition = new SmsApiV1Condition();
condition.setApikey(smsProperty.getApikey());
if (messageContent == null || messageContent.trim().equals(""))
condition.setContent(URLEncoder.encode(content(mobile), "UTF-8"));
else
condition.setContent(URLEncoder.encode(messageContent, "UTF-8"));
condition.setMobile(mobile);
condition.setUsername(smsProperty.getUsername());
String md5Password = bytesToHex(MessageDigest.getInstance("MD5").digest(smsProperty.getPassword().getBytes("UTF-8")));
condition.setPassword_md5(md5Password);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(condition);
} catch (UnsupportedEncodingException | NoSuchAlgorithmException | JsonProcessingException e) {
log.error("发送短信失败", e);
throw new SmsException("发送短信失败", SmsResponseEnum.SEND_ERROR);
}
}
private String content(String mobile) {
String captcha = RandomDigitGenerator.generate();
return String.format(smsProperty.getTemplate(), captcha);
}
/**
* 二进制转十六进制
*
* @param bytes
* @return
*/
public static String bytesToHex(byte[] bytes) {
StringBuilder md5str = new StringBuilder();
//把数组每一字节换成16进制连成md5字符串
int digital;
for (int i = 0; i < bytes.length; i++) {
digital = bytes[i];
if (digital < 0) {
digital += 256;
}
if (digital < 16) {
md5str.append("0");
}
md5str.append(Integer.toHexString(digital));
}
return md5str.toString().toLowerCase();
}
}
package com.qkdata.sms.service;
import com.qkdata.sms.exception.SmsException;
import com.qkdata.sms.constant.SmsResponseEnum;
import com.qkdata.sms.constant.SmsTemplate;
import com.qkdata.sms.model.InsertTemplateCondition;
import com.qkdata.sms.model.SmsTypeAndChannelDTO;
import com.qkdata.sms.repository.SmsTemplateMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TemplateService {
@Autowired
private SmsTemplateMapper smsTemplateMapper;
public List<SmsTemplate> smsTemplateList(){
return smsTemplateMapper.selectAll();
}
public Integer insertTemplate(InsertTemplateCondition insertTemplateCondition) throws Exception{
if (checkTemplateExist(insertTemplateCondition.getCode())){
throw new SmsException("短信模板code已存在", SmsResponseEnum.TEMPLATE_CODE_ISEXIST);
}
SmsTemplate smsTemplate = new SmsTemplate();
smsTemplate.setCode(insertTemplateCondition.getCode());
smsTemplate.setType(insertTemplateCondition.getType());
smsTemplate.setChannel(insertTemplateCondition.getChannel());
smsTemplate.setAlibabaSignName(insertTemplateCondition.getAlibabaSignName());
smsTemplate.setAlibabaTemplateCode(insertTemplateCondition.getAlibabaTemplateCode());
smsTemplate.setAlibabaTemplateContent(insertTemplateCondition.getAlibabaTemplateContent());
smsTemplate.setLmobileSignName(insertTemplateCondition.getLmobileSignName());
smsTemplate.setLmobileTemplateContent(insertTemplateCondition.getLmobileTemplateContent());
smsTemplateMapper.insertSelective(smsTemplate);
return smsTemplate.getId();
}
private Boolean checkTemplateExist(String code){
int count = smsTemplateMapper.selectCount(new SmsTemplate(code));
if (count >= 1){
return true;
}
return false;
}
public SmsTemplate getTemplateByCode(String code){
SmsTemplate smsTemplate = smsTemplateMapper.selectOne(new SmsTemplate(code));
return smsTemplate;
}
public Integer getTemplateCount(List<String> codeList){
return smsTemplateMapper.countTemplateByCode(codeList);
}
public SmsTypeAndChannelDTO getTypeAndChannelByCode(String code) {
return smsTemplateMapper.getTypeAndChannelByCode(code);
}
}
package com.qkdata.sms.util;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
/**
* @author chenjiahai
*/
@Slf4j
public class JaxbUtils {
private static JAXBContext jaxbContext;
//xml转java对象
@SuppressWarnings("unchecked")
public static <T> T xmlToBean(HttpServletRequest request, Class<T> c) {
T t = null;
try {
jaxbContext = JAXBContext.newInstance(c);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
t = (T) unmarshaller.unmarshal(request.getInputStream());
} catch (JAXBException e) {
log.error("JAXB异常", e);
} catch (IOException e) {
log.error("获取request中的inputStream失败", e);
}
return t;
}
public static <T> T xmlToBean(String xml, Class<T> c) {
T t = null;
try {
jaxbContext = JAXBContext.newInstance(c);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
t = (T) unmarshaller.unmarshal(new StringReader(xml));
} catch (JAXBException e) {
log.error("JAXB异常", e);
}
return t;
}
//java对象转xml
public static String beanToXml(Object obj) {
return beanToXml(obj, false);
}
public static String beanToXml(Object obj, boolean format) {
StringWriter writer = null;
try {
jaxbContext = JAXBContext.newInstance(obj.getClass());
Marshaller marshaller = jaxbContext.createMarshaller();
//Marshaller.JAXB_FRAGMENT:是否省略xml头信息,true省略,false不省略
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
//Marshaller.JAXB_FORMATTED_OUTPUT:决定是否在转换成xml时同时进行格式化(即按标签自动换行,否则即是一行的xml)
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, format);
//Marshaller.JAXB_ENCODING:xml的编码方式
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
writer = new StringWriter();
marshaller.marshal(obj, writer);
} catch (JAXBException e) {
log.error("JAXB-Java对象转XML", e);
return "";
}
return writer.toString();
}
}
\ No newline at end of file
package com.qkdata.sms.util;
import java.util.Random;
public class RandomDigitGenerator {
private static final String DIGITS = "0123456789";
private static final int DEFAULT_LENGTH = 6;
private RandomDigitGenerator() {
}
public static String generate() {
return generate(DEFAULT_LENGTH);
}
public static String generate(int length) {
Random random = new Random();
StringBuilder verifyCode = new StringBuilder();
for (int i = 0; i < length; i++) {
verifyCode.append(DIGITS.charAt(random.nextInt(DIGITS.length() - 1)));
}
return verifyCode.toString();
}
}
server:
port: 9004
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/sms?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
username: root
password: 123456
redis:
host: localhost
log:
context: sms
path: /Users/liuyang/work/argus_work/online-edu/data/logs
server:
port: 80
servlet:
context-path: /sms
spring:
datasource:
druid:
url: jdbc:mysql://mysql/sms?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
username: root
password: qkdata
driver-class-name: com.mysql.jdbc.Driver
initialSize: 3
minIdle: 2
maxActive: 10
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: false
maxPoolPreparedStatementPerConnectionSize: -1
filters: stat,slf4j,config
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMil=3000
useGlobalDataSourceStat: true
filter:
wall:
config:
multi-statement-allow: true
redis:
host: redis
port: 6379
database: 10
timeout: 2s
password:
jedis:
pool:
min-idle: 1
max-idle: 8
max-active: 8
# max-wait: -1s
flyway:
baseline-on-migrate: true
placeholder-replacement: false
mybatis:
mapper-locations: classpath*:mappers/*.xml
type-handlers-package: com.qkdata.sms.config
log:
context: sms
path: /data/logs
aliyun:
accessKeyId: LTAI4GCTRFRxud86a58AoV3X
accessKeySecret: Rpwj9DBwJpNWzwjcFeffLWqEuJ56i0
lmobile:
user: todo
password: todo
productId: todo
url: http://api.51welink.com/json/sms/g_Submit
retry:
count: 5
DROP TABLE IF EXISTS `sms_template`;
CREATE TABLE `sms_template` (
`id` bigint(10) NOT NULL AUTO_INCREMENT,
`code` varchar(50) NOT NULL COMMENT '模板code',
`type` tinyint(2) NOT NULL COMMENT '模板类型',
`channel` tinyint(2) NOT NULL COMMENT '优先使用平台',
`status` tinyint(2) DEFAULT NULL COMMENT '模板状态',
`alibaba_template_code` varchar(50) NOT NULL COMMENT '阿里短信code',
`alibaba_sign_name` varchar(50) NOT NULL COMMENT '阿里签名',
`lmobile_sign_name` varchar(50) NOT NULL COMMENT '乐信通签名',
`alibaba_template_content` varchar(255) NOT NULL COMMENT '阿里短信模板',
`lmobile_template_content` varchar(255) NOT NULL COMMENT '乐信通短信模板',
`create_at` datetime DEFAULT CURRENT_TIMESTAMP,
`update_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sms_template
-- ----------------------------
INSERT INTO `sms_template` VALUES (1, 'T_LG_CAPTCHA', 1, 1, 1, 'SMS_190520446', '乾坤数据', '', '验证码${code},您正在注册成为新用户,感谢您的支持!', '', NULL, NULL);
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 获取application配置文件中的配置 -->
<!--<springProperty scope="context" name="LOG_LEVEL" source="log.level"/>-->
<springProperty scope="context" name="LOG_PATH" source="log.path"/>
<springProperty scope="context" name="LOG_CONTEXT" source="log.context"/>
<property name="PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%X{traceId}] %-5level %logger{50} - %line %msg %n"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_PATH}/${LOG_CONTEXT}-error.log</File>
<!-- 过滤器,只记录ERROR级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_CONTEXT}-error.log.%i.%d{yyyy-MM-dd}</fileNamePattern>
<MaxHistory>30</MaxHistory>
<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>30MB</maxFileSize>
</TimeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_PATH}/${LOG_CONTEXT}.log</File>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_CONTEXT}.log.%i.%d{yyyy-MM-dd}</fileNamePattern>
<maxHistory>30</maxHistory>
<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>30MB</maxFileSize>
</TimeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<Pattern>${PATTERN}</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="SQL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_PATH}/${LOG_CONTEXT}-sql.log</File>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_CONTEXT}-sql.log.%i.%d{yyyy-MM-dd}</fileNamePattern>
<maxHistory>30</maxHistory>
<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>30MB</maxFileSize>
</TimeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<Pattern>${PATTERN}</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="INFO"/>
<appender-ref ref="ERROR"/>
<appender-ref ref="STDOUT"/>
</root>
<logger name="org" level="INFO" additivity="false">
<appender-ref ref="INFO"/>
<appender-ref ref="ERROR"/>
<appender-ref ref="STDOUT"/>
</logger>
<logger name="com.shangwei" level="INFO" additivity="false">
<appender-ref ref="INFO"/>
<appender-ref ref="ERROR"/>
<appender-ref ref="STDOUT"/>
</logger>
<logger name="druid.sql.Statement" level="DEBUG" additivity="false">
<appender-ref ref="SQL"/>
<appender-ref ref="STDOUT"/>
</logger>
</configuration>
\ No newline at end of file
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qkdata.sms.repository.SmsTemplateMapper">
<resultMap id="smsTypeAndChannelDTO" type="com.qkdata.sms.model.SmsTypeAndChannelDTO">
<result property="type" column="type"/>
<result property="channel" column="channel"/>
</resultMap>
<select id="countTemplateByCode" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM sms_template
WHERE code IN
<foreach item="code" index="index" collection="codeList"
open="(" separator="," close=")">
#{code}
</foreach>
</select>
<select id="getTypeAndChannelByCode" resultMap="smsTypeAndChannelDTO">
SELECT type, channel
FROM sms_template
WHERE code = #{code}
</select>
</mapper>
\ No newline at end of file
package com.qkdata.sms;
import com.qkdata.sms.constant.SmsTemplate;
import com.qkdata.sms.constant.SmsChannelEnum;
import com.qkdata.sms.constant.SmsTypeEnum;
import com.qkdata.sms.repository.SmsTemplateMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Profile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
@ActiveProfiles(value = "dev")
public class SmsTemplateMapperTest {
@Autowired
private SmsTemplateMapper smsTemplateMapper;
@Test
public void testAdd() {
SmsTemplate smsTemplate = new SmsTemplate();
smsTemplate.setCode("YL_CAPTCHA_1");
smsTemplate.setType(SmsTypeEnum.CAPTCHA);
smsTemplate.setChannel(SmsChannelEnum.ALIBABA);
smsTemplate.setStatus(1);
smsTemplateMapper.insertSelective(smsTemplate);
}
@Test
public void testSelectOne() {
String code = "T_LG_CAPTCHA";
SmsTemplate smsTemplate = smsTemplateMapper.selectOne(new SmsTemplate(code));
assertNotNull(smsTemplate);
assertEquals("T_LG_CAPTCHA", smsTemplate.getCode());
assertSame(SmsTypeEnum.CAPTCHA, smsTemplate.getType());
assertSame(SmsChannelEnum.ALIBABA, smsTemplate.getChannel());
}
}
package com.qkdata.sms;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qkdata.sms.service.RedisService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
public class SmsTest {
@Autowired
private RedisService redisService;
private ObjectMapper objectMapper = new ObjectMapper();
@Test
public void testLpush() {
}
}
package com.qkdata.sms;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.stream.LongStream;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
@RunWith(SpringRunner.class)
public class SplitMobileTest {
private String singleMobile;
private String mobiles;
@Before
public void before() {
this.singleMobile = "15822248411";
this.mobiles = LongStream.range(10000000000L, 19999999999L)
.limit(100)
.boxed()
.map(String::valueOf)
.collect(joining(","));
this.mobiles = this.mobiles.replace("10000000000", "");
}
@Test
public void testSplitOfSingleMobile() {
List<String> mobiles = split(singleMobile);
assertSame(1, mobiles.size());
assertEquals("15822248411", mobiles.get(0));
}
@Test
public void testSpliteOfMobiles() {
List<String> mobiles = split(this.mobiles);
assertSame(99, mobiles.size());
assertEquals("10000000001", mobiles.get(0));
assertEquals("10000000099", mobiles.get(mobiles.size() - 1));
}
private List<String> split(String mobile) {
return Arrays.stream(mobile.split(","))
.map(String::trim)
.filter(x -> !StringUtils.isEmpty(x))
.collect(toList());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment