first commit.

This commit is contained in:
2022-12-07 18:14:38 +08:00
commit e916d067f3
183 changed files with 9649 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
package xyz.zhouxy.plusone.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* Plusone 配置。加载 plusone.properties 文件
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
@PropertySource(value = {"classpath:conf/plusone.properties"}, encoding = "utf-8")
public class PlusoneConfig {
}

View File

@@ -0,0 +1,41 @@
package xyz.zhouxy.plusone.infrastructure.pojo;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 实体的持久化对象
*
* <p>
* <i>由于本项目目前是手动实现 Repository直接将实体持久化
* 查询结果也是直接实例化为 Entity故暂时不使用 PO。</i>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@ToString
public abstract class AbstractEntityPO {
protected Long createdBy;
protected LocalDateTime createTime;
protected Long updatedBy;
protected LocalDateTime updateTime;
public abstract Long getId();
public void auditFields(Long updatedBy, LocalDateTime updateTime) {
this.updatedBy = updatedBy;
this.updateTime = updateTime;
}
public void auditFields(Long createdBy, LocalDateTime createTime, Long updatedBy, LocalDateTime updateTime) {
this.createdBy = createdBy;
this.createTime = createTime;
this.updatedBy = updatedBy;
this.updateTime = updateTime;
}
}

View File

@@ -0,0 +1,33 @@
package xyz.zhouxy.plusone.jdbc;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.lang.Nullable;
/**
* 扩展了 {@link BeanPropertySqlParameterSource},在将 POJO 转换为
* {@link org.springframework.jdbc.core.namedparam.SqlParameterSource} 时,
* 使用 {@link Enum#ordinal()} 将枚举转换成整数。
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see SqlParameterSource
* @see BeanPropertySqlParameterSource
* @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
* @see Enum
*/
public class BeanPropertyParamSource extends BeanPropertySqlParameterSource {
public BeanPropertyParamSource(Object object) {
super(object);
}
@Override
@Nullable
public Object getValue(String paramName) throws IllegalArgumentException {
Object value = super.getValue(paramName);
if (value instanceof Enum) {
return ((Enum<?>) value).ordinal();
}
return value;
}
}

View File

@@ -0,0 +1,51 @@
package xyz.zhouxy.plusone.jdbc;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
import org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration;
import xyz.zhouxy.plusone.jdbc.converter.EnumToOrdinalConverter;
/**
* JDBC 配置
*
* <p>
* 配置了类型转换器
* </p>
*
* <p>
* 这里声明了 {@link DefaultQueryMappingConfiguration} 的实例为 Spring bean。
* 在其它配置类中注入该 bean就可以向其中添加
* {@link org.springframework.jdbc.core.RowMapper}
* 供 Spring Data JDBC 查询时使用。如下所示:
*
* <pre>
* {@code @Configuration}
* class CustomConfig {
* public CustomConfig(DefaultQueryMappingConfiguration rowMappers) {
* rowMappers.registerRowMapper(Person.class, new PersonRowMapper());
* }
* }
* </pre>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
public class JdbcConfig extends AbstractJdbcConfiguration {
@Override
public JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(List.of(EnumToOrdinalConverter.INSTANCE));
}
@Bean
QueryMappingConfiguration rowMappers() {
return new DefaultQueryMappingConfiguration();
}
}

View File

@@ -0,0 +1,68 @@
package xyz.zhouxy.plusone.jdbc;
import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import xyz.zhouxy.plusone.domain.Entity;
public abstract class JdbcEntityDaoSupport<T extends Entity<ID>, ID extends Serializable> {
protected final NamedParameterJdbcTemplate jdbc;
protected RowMapper<T> rowMapper;
protected ResultSetExtractor<T> resultSetExtractor;
protected JdbcEntityDaoSupport(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.jdbc = namedParameterJdbcTemplate;
this.rowMapper = (ResultSet rs, int rowNum) -> mapRow(rs);
this.resultSetExtractor = (ResultSet rs) -> rs.next() ? mapRow(rs) : null;
}
protected final T queryForObject(String sql) {
return this.jdbc.query(sql, this.resultSetExtractor);
}
protected final T queryForObject(String sql, SqlParameterSource paramSource) {
return this.jdbc.query(sql, paramSource, this.resultSetExtractor);
}
protected final List<T> queryForList(String sql) {
return this.jdbc.query(sql, this.rowMapper);
}
protected final List<T> queryForList(String sql, SqlParameterSource parameterSource) {
return this.jdbc.query(sql, parameterSource, this.rowMapper);
}
protected final Stream<T> queryForStream(String sql, SqlParameterSource parameterSource) {
return this.jdbc.queryForStream(sql, parameterSource, this.rowMapper);
}
protected final <E> Stream<E> queryForStream(String sql, SqlParameterSource parameterSource, Class<E> elementType) {
return this.jdbc.queryForList(sql, parameterSource, elementType).stream();
}
protected final boolean queryExists(String sql, SqlParameterSource parameterSource) {
Boolean isExists = this.jdbc.query(sql, parameterSource, ResultSet::next);
return Boolean.TRUE.equals(isExists);
}
protected abstract T mapRow(ResultSet rs) throws SQLException;
protected void setRowMapper(@Nonnull RowMapper<T> rowMapper) {
this.rowMapper = rowMapper;
}
protected void setResultSetExtractor(@Nonnull ResultSetExtractor<T> resultSetExtractor) {
this.resultSetExtractor = resultSetExtractor;
}
}

View File

@@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.jdbc;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import xyz.zhouxy.plusone.spring.SpringContextHolder;
/**
* 全局单例,由 Spring 装配好的对象。
* 可通过静态方法获取 Spring 容器中的 {@link JdbcTemplate} 和
* {@link NamedParameterJdbcTemplate} 对象。
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see JdbcTemplate
* @see NamedParameterJdbcTemplate
*/
public final class JdbcFactory {
private JdbcFactory() {
throw new IllegalStateException("Utility class");
}
public static JdbcTemplate getJdbcTemplate() {
return SpringContextHolder.getContext().getBean(JdbcTemplate.class);
}
public static NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() {
return SpringContextHolder.getContext().getBean(NamedParameterJdbcTemplate.class);
}
}

View File

@@ -0,0 +1,58 @@
package xyz.zhouxy.plusone.jdbc;
import java.io.Serializable;
import javax.annotation.Nonnull;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import xyz.zhouxy.plusone.domain.AggregateRoot;
import xyz.zhouxy.plusone.domain.IRepository;
public abstract class JdbcRepositorySupport<T extends AggregateRoot<ID>, ID extends Serializable>
extends JdbcEntityDaoSupport<T, ID>
implements IRepository<T, ID> {
protected JdbcRepositorySupport(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
super(namedParameterJdbcTemplate);
}
protected abstract void doDelete(@Nonnull T entity);
protected abstract T doFindById(@Nonnull ID id);
protected abstract T doInsert(@Nonnull T entity);
protected abstract T doUpdate(@Nonnull T entity);
@Override
public final void delete(T entity) {
if (entity == null) {
throw new IllegalArgumentException("Cannot delete null.");
}
doDelete(entity);
}
@Override
public final T find(ID id) {
if (id == null) {
throw new IllegalArgumentException("Id cannot be null.");
}
return doFindById(id);
}
@Override
public final T save(T entity) {
if (entity == null) {
throw new IllegalArgumentException("Cannot save null.");
}
return entity.getId().isPresent() ? doUpdate(entity) : doInsert(entity);
}
protected abstract SqlParameterSource generateParamSource(ID id, @Nonnull T entity);
protected final SqlParameterSource generateParamSource(@Nonnull T entity) {
return generateParamSource(entity.getId().orElseThrow(), entity);
}
}

View File

@@ -0,0 +1,45 @@
package xyz.zhouxy.plusone.jdbc.common;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 查询结果处理
*
* <p>
* 通过在 {@link #map(ResultSet)} 中配置 {@link ResultSet} 到对象的映射,
* 可将 {@link #rowMapper(ResultSet, int)} 的方法应用,
* 直接当成 {@link org.springframework.jdbc.core.RowMapper} 对象传给
* {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}
* 的查询方法,
* 或者在 Spring Data JDBC 的配置中使用。
* </p>
*
* <p>
* 在
* {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}
* 的 query(String, java.util.Map, ResultSetExtractor)
* 和 query(String, SqlParameterSource, ResultSetExtractor)
* 两个方法执行时ResultSetExtractor 中需要执行一次 {@link ResultSet#next()} 判断是否查询到数据,
* 如果查询到数据再执行查询结果的实例化。
* {@link #resultSetExtractor(ResultSet)} 封装了这个过程,
* 和 {@link #rowMapper(ResultSet, int)}一样,
* 直接把方法引用当成 {@link org.springframework.jdbc.core.ResultSetExtractor} 的对象即可。
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration
*/
@FunctionalInterface
public interface ResultMapper<T> {
T map(ResultSet resultSet) throws SQLException;
default T rowMapper(ResultSet resultSet, int rowNum) throws SQLException {
return map(resultSet);
}
default T resultSetExtractor(ResultSet resultSet) throws SQLException {
return resultSet.next() ? map(resultSet) : null;
}
}

View File

@@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.jdbc.common;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
public class SimpleResultMapper<T> implements ResultMapper<T> {
private final RowMapper<T> rowMapper;
public SimpleResultMapper(RowMapper<T> rowMapper) {
this.rowMapper = rowMapper;
}
@Override
public T map(ResultSet resultSet) throws SQLException {
return rowMapper.mapRow(resultSet, 1);
}
@Override
public T rowMapper(ResultSet resultSet, int rowNum) throws SQLException {
return this.rowMapper.mapRow(resultSet, rowNum);
}
public static <T> SimpleResultMapper<T> of(Class<T> clazz) {
return new SimpleResultMapper<>(new BeanPropertyRowMapper<>(clazz));
}
}

View File

@@ -0,0 +1,20 @@
package xyz.zhouxy.plusone.jdbc.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;
/**
* 枚举序号转换器
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@WritingConverter
public enum EnumToOrdinalConverter implements Converter<Enum<?>, Integer> {
INSTANCE
;
@Override
public Integer convert(Enum<?> source) {
return source.ordinal();
}
}

View File

@@ -0,0 +1,44 @@
package xyz.zhouxy.plusone.jdbc.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
/**
* 序号枚举转换器
* <p>
* 向 {@link org.springframework.data.jdbc.core.convert.JdbcCustomConversions}
* 中添加类型转换器,
* 如:
*
* <pre>
* return new JdbcCustomConversions(List.of(new OrdinalToEnumConverter<SystemStatus>()));
* </pre>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see Converter
* @see org.springframework.data.jdbc.core.convert.JdbcCustomConversions
*/
@ReadingConverter
public class OrdinalToEnumConverter<E extends Enum<E>> implements Converter<Integer, Enum<E>> {
private final Class<E> type;
private final E[] constants;
public OrdinalToEnumConverter(Class<E> type) {
this.type = type;
this.constants = type.getEnumConstants();
}
@Override
public Enum<E> convert(Integer ordinal) {
try {
return constants[ordinal];
} catch (ArrayIndexOutOfBoundsException exception) {
throw new IllegalArgumentException(
String.format("Cannot convert %d to %s by ordinal value.", ordinal, type.getSimpleName()),
exception);
}
}
}

View File

@@ -0,0 +1,31 @@
package xyz.zhouxy.plusone.mail;
import org.springframework.mail.SimpleMailMessage;
/**
* 构建 {@link SimpleMailMessage}
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class MailMessageFactory {
private final PlusoneMailProperties mailProperties;
public MailMessageFactory(PlusoneMailProperties mailProperties) {
this.mailProperties = mailProperties;
}
public SimpleMailMessage getCodeMailMessage(String code, String to) {
String subject = mailProperties.getSubject().get("code");
String content = String.format(mailProperties.getTemplate().get("code"), code);
return getSimpleMailMessage(to, subject, content);
}
public SimpleMailMessage getSimpleMailMessage(String to, String subject, String content) {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setFrom(mailProperties.getFrom());
simpleMailMessage.setTo(to);
simpleMailMessage.setSubject(subject);
simpleMailMessage.setText(content);
return simpleMailMessage;
}
}

View File

@@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.mail;
import javax.mail.MessagingException;
/**
* 邮件服务
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface MailService {
void sendSimpleMail(String to, String subject, String content);
void sendHtmlMail(String to, String subject, String content) throws MessagingException;
void sendCodeMail(String code, String to);
}

View File

@@ -0,0 +1,26 @@
package xyz.zhouxy.plusone.mail;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
/**
* 读取配置文件,实例化 {@link MailService} 对象后,放到 Spring 容器中。
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
@EnableConfigurationProperties(PlusoneMailProperties.class)
@ConditionalOnClass(MailService.class)
@EnableAutoConfiguration
public class PlusoneMailAutoConfiguration {
@Bean
public MailService mailService(JavaMailSender mailSender, PlusoneMailProperties mailProperties) {
MailMessageFactory mailMessageFactory = new MailMessageFactory(mailProperties);
return new SimpleMailService(mailSender, mailMessageFactory);
}
}

View File

@@ -0,0 +1,16 @@
package xyz.zhouxy.plusone.mail;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Map;
@Getter
@Setter
@ConfigurationProperties("plusone.mail")
public class PlusoneMailProperties {
private String from;
private Map<String, String> subject;
private Map<String, String> template;
}

View File

@@ -0,0 +1,57 @@
package xyz.zhouxy.plusone.mail;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import javax.mail.MessagingException;
/**
* 邮件服务。<b>不能发送 HTML 邮件。</b>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Slf4j
public class SimpleMailService implements MailService {
private final JavaMailSender mailSender;
private final MailMessageFactory mailMessageFactory;
public SimpleMailService(JavaMailSender mailSender, MailMessageFactory mailMessageFactory) {
this.mailSender = mailSender;
this.mailMessageFactory = mailMessageFactory;
}
/**
* 发送文本邮件
*/
@Override
public void sendSimpleMail(String to, String subject, String content) {
SimpleMailMessage message = mailMessageFactory.getSimpleMailMessage(to, subject, content);
try {
mailSender.send(message);
log.debug("简单邮件已经发送。");
} catch (MailException e) {
log.error("发送简单邮件时发生异常!", e);
throw e;
}
}
@Override
public void sendHtmlMail(String to, String subject, String content) throws MessagingException {
throw new UnsupportedOperationException("暂不支持");
}
@Override
public void sendCodeMail(String code, String to) {
SimpleMailMessage message = mailMessageFactory.getCodeMailMessage(code, to);
try {
mailSender.send(message);
log.debug("简单邮件已经发送。");
} catch (MailException e) {
log.error("发送简单邮件时发生异常!", e);
throw e;
}
}
}

View File

@@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.mybatis;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class MyBatisAutoConfiguration {
@Bean
MybatisUtil mybatisUtil(SqlSessionFactory sqlSessionFactory) {
return MybatisUtil.getInstance()
.setSqlSessionFactory(sqlSessionFactory);
}
}

View File

@@ -0,0 +1,19 @@
package xyz.zhouxy.plusone.mybatis;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

View File

@@ -0,0 +1,28 @@
package xyz.zhouxy.plusone.mybatis;
import org.apache.ibatis.session.SqlSessionFactory;
public final class MybatisUtil {
private SqlSessionFactory sqlSessionFactory;
private MybatisUtil() {
}
MybatisUtil setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
return this;
}
private static final class Holder {
private static final MybatisUtil INSTANCE = new MybatisUtil();
}
public static MybatisUtil getInstance() {
return Holder.INSTANCE;
}
public static SqlSessionFactory getSqlSessionFactory() {
return MybatisUtil.getInstance().sqlSessionFactory;
}
}

View File

@@ -0,0 +1,42 @@
package xyz.zhouxy.plusone.redis;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;
import java.util.concurrent.TimeUnit;
/**
* 使用 Redis 存放临时字符串
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Repository
public class RedisStrCacheDAO implements StrCacheDAO {
private final StringRedisTemplate template;
public RedisStrCacheDAO(StringRedisTemplate template) {
this.template = template;
}
@Override
public void setValue(String key, String value, long timeout, TimeUnit unit) {
ValueOperations<String, String> ops = template.opsForValue();
ops.set(key, value, timeout, unit);
}
@Override
public String getValue(String key) {
ValueOperations<String, String> ops = this.template.opsForValue();
return ops.get(key);
}
@Override
public String getValueAndDelete(String key) {
ValueOperations<String, String> ops = this.template.opsForValue();
String value = ops.get(key);
template.delete(key);
return value;
}
}

View File

@@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.redis;
import java.util.concurrent.TimeUnit;
/**
* 字符串缓存接口
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface StrCacheDAO {
void setValue(String key, String value, long timeout, TimeUnit unit);
String getValue(String key);
String getValueAndDelete(String key);
}

View File

@@ -0,0 +1,27 @@
package xyz.zhouxy.plusone.sms;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* SMS 相关配置
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
@EnableConfigurationProperties(value = {
SmsProperties.class,
SmsCredentialProperties.class,
SmsClientProperties.class,
SmsHttpProperties.class,
SmsProxyProperties.class})
@ConditionalOnClass(SmsService.class)
public class PlusoneSmsAutoConfiguration {
@Bean
public SmsService smsService(SmsProperties smsProperties) {
return new TencentSmsServiceImpl(smsProperties);
}
}

View File

@@ -0,0 +1,51 @@
package xyz.zhouxy.plusone.sms;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;
/**
* SMS 相关参数
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
@ConfigurationProperties("plusone.sms")
public class SmsProperties {
private String region;
private SmsCredentialProperties credential;
private SmsClientProperties client;
private String appId;
private Map<String, String> templates;
}
@Data
@ConfigurationProperties("plusone.sms.credential")
class SmsCredentialProperties {
private String secretId;
private String secretKey;
}
@Data
@ConfigurationProperties("plusone.sms.client")
class SmsClientProperties {
private String signMethod;
private SmsHttpProperties http;
}
@Data
@ConfigurationProperties("plusone.sms.client.http")
class SmsHttpProperties {
private SmsProxyProperties proxy;
private String reqMethod;
private Integer connTimeout;
}
@Data
@ConfigurationProperties("plusone.sms.client.http.proxy")
class SmsProxyProperties {
private String host;
private Integer port;
}

View File

@@ -0,0 +1,13 @@
package xyz.zhouxy.plusone.sms;
/**
* SMS 服务
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface SmsService {
void sendCodeMessage(String code, String phoneNumber);
void sendCodeMessage(String signName, String code, String phoneNumber);
}

View File

@@ -0,0 +1,145 @@
package xyz.zhouxy.plusone.sms;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
/**
* 使用腾讯 SMS 服务
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
@Slf4j
public class TencentSmsServiceImpl implements SmsService {
private final SmsProperties properties;
private final Credential credential;
public TencentSmsServiceImpl(SmsProperties properties) {
this.properties = properties;
SmsCredentialProperties smsCredential = properties.getCredential();
this.credential = new Credential(smsCredential.getSecretId(), smsCredential.getSecretKey());
}
@Override
public void sendCodeMessage(String code, String phoneNumber) {
sendCodeMessage("ZhouXY", code, phoneNumber);
}
@Override
public void sendCodeMessage(String signName, String code, String phoneNumber) {
String[] phoneNums = {"+86" + phoneNumber};
sendMessage(signName, "code", phoneNums, code, "10");
}
public void sendMessage(String signName, String action, String[] phoneNumberSet, String... templateParamSet) {
try {
SmsClient client = getClient();
SendSmsRequest req = getSendSmsRequest(signName,
properties.getTemplates().get(action),
phoneNumberSet,
templateParamSet);
/*
* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的 返回的 res 是一个
* SendSmsResponse 类的实例,与请求对象对应
*/
// var res = client.SendSms(req);
client.SendSms(req);
// 输出json格式的字符串回包
// System.out.println(SendSmsResponse.toJsonString(res));
// 也可以取出单个值你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义
// System.out.println(res.getRequestId());
} catch (TencentCloudSDKException e) {
log.error(e.getMessage(), e);
}
}
private SendSmsRequest getSendSmsRequest(String signName,
String templateId,
String[] phoneNumberSet,
String... templateParamSet) {
SendSmsRequest req = new SendSmsRequest();
String sdkAppId = properties.getAppId();
req.setSmsSdkAppId(sdkAppId);
req.setSignName(signName);
/* 国际/港澳台短信 SenderId: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */
String senderId = "";
req.setSenderId(senderId);
/* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息server 会原样返回 */
// String sessionContext = "xxx";
// req.setSessionContext(sessionContext);
/* 短信号码扩展号: 默认未开通,如需开通请联系 [sms helper] */
String extendCode = "";
req.setExtendCode(extendCode);
/* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */
req.setTemplateId(templateId);
/*
* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号] 示例如:+8613711112222 其中前面有一个+号
* 86为国家码13711112222为手机号最多不要超过200个手机号
*/
req.setPhoneNumberSet(phoneNumberSet);
/* 模板参数: 若无模板参数,则设置为空 */
req.setTemplateParamSet(templateParamSet);
return req;
}
private SmsClient getClient() {
String region = properties.getRegion();
ClientProfile clientProfile = getClientProfile();
return new SmsClient(credential, region, clientProfile);
}
private ClientProfile getClientProfile() {
HttpProfile httpProfile = getHttpProfile();
/*
* 非必要步骤: 实例化一个客户端配置对象,可以指定超时时间等配置
*/
ClientProfile clientProfile = new ClientProfile();
String signMethod = properties.getClient().getSignMethod();
if (signMethod != null) {
clientProfile.setSignMethod(signMethod);
} else {
clientProfile.setSignMethod("HmacSHA256");
}
clientProfile.setHttpProfile(httpProfile);
return clientProfile;
}
private HttpProfile getHttpProfile() {
HttpProfile httpProfile = new HttpProfile();
SmsHttpProperties smsHttp = properties.getClient().getHttp();
if (smsHttp != null) {
if (smsHttp.getReqMethod() != null) {
httpProfile.setReqMethod(smsHttp.getReqMethod());
}
if (smsHttp.getConnTimeout() != null) {
httpProfile.setConnTimeout(60);
}
SmsProxyProperties proxy = smsHttp.getProxy();
if (proxy != null && proxy.getHost() != null && proxy.getPort() != null) {// 设置代理
httpProfile.setProxyHost(proxy.getHost());
httpProfile.setProxyPort(proxy.getPort());
}
}
return httpProfile;
}
}

View File

@@ -0,0 +1,28 @@
package xyz.zhouxy.plusone.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class SpringContextHolder {
private ApplicationContext context;
private static final SpringContextHolder INSTANCE = new SpringContextHolder();
private SpringContextHolder() {
}
public static ApplicationContext getContext() {
return INSTANCE.context;
}
@Configuration
static class SpringContextHolderConfig {
@Bean(name = "springContextHolder")
SpringContextHolder getSpringContextHolder(ApplicationContext context) {
SpringContextHolder.INSTANCE.context = context;
return SpringContextHolder.INSTANCE;
}
}
}

View File

@@ -0,0 +1,34 @@
package xyz.zhouxy.plusone.sql;
import java.util.Objects;
import org.apache.ibatis.jdbc.AbstractSQL;
/**
* 扩展 MyBatis 中的 SQL 语句构造器
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class SQL extends AbstractSQL<SQL> {
public SQL SET_IF(boolean condition, String sets) {
return condition ? SET(sets) : getSelf();
}
public SQL SET_IF_NOT_NULL(Object param, String sets) {
return Objects.nonNull(param) ? SET(sets) : getSelf();
}
public SQL WHERE_IF(boolean condition, String sqlCondition) {
return condition ? WHERE(sqlCondition) : getSelf();
}
public SQL WHERE_IF_NOT_NULL(Object param, String sqlCondition) {
return Objects.nonNull(param) ? WHERE(sqlCondition) : getSelf();
}
@Override
public SQL getSelf() {
return this;
}
}

View File

@@ -0,0 +1,72 @@
package xyz.zhouxy.plusone.validator;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import xyz.zhouxy.plusone.exception.InvalidInputException;
/**
* 校验器
*
* <p>可以使用以下方式初始化一个校验器:</p>
*
* <pre>
* BaseValidator&lt;Integer&gt; validator = new BaseValidator&lt;&gt;() {
* {
* ruleFor(value -> Objects.nonNull(value), "value 不能为空");
* ruleFor(value -> (value >= 0 && value <= 500), "value 应在 [0, 500] 内");
* }
* };
* </pre>
*
* <p>也可以通过继承本类,定义一个校验器(可使用单例模式)。</p>
*
* <p>
* 然后通过校验器的 {@link #validate} 方法,或
* {@link ValidateUtil#validate(Object, Validator)} 对指定对象进行校验。
* </p>
*
* <pre>
* ValidateUtil.validate(255, validator);
* </pre>
*
* <pre>
* validator.validate(666);
* </pre>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see IValidateRequired
* @see ValidateUtil
* @see Validator
*/
public abstract class BaseValidator<T> {
private final List<RuleInfo<T>> rules = new ArrayList<>();
protected BaseValidator() {
}
protected final void ruleFor(Predicate<T> rule, String errorMessage) {
this.rules.add(new RuleInfo<>(rule, errorMessage));
}
public void validate(T obj) {
this.rules.forEach((RuleInfo<T> ruleInfo) -> {
if (!ruleInfo.rule.test(obj)) {
throw new InvalidInputException(ruleInfo.message);
}
});
}
protected static class RuleInfo<T> {
Predicate<T> rule;
String message;
public RuleInfo(Predicate<T> rule, String message) {
this.rule = rule;
this.message = message;
}
}
}

View File

@@ -0,0 +1,129 @@
package xyz.zhouxy.plusone.validator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import cn.hutool.core.exceptions.ValidateException;
import lombok.AllArgsConstructor;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.util.RegexUtil;
public abstract class BaseValidator2<T> {
private List<ValidValueHolder<T, ?>> hs = new ArrayList<>();
protected final <R> ValidValueHolder<T, R> ruleFor(Function<T, R> getter) {
ValidValueHolder<T, R> validValueHolder = new ValidValueHolder<>(getter);
hs.add(validValueHolder);
return validValueHolder;
}
public void validate(T obj) {
for (var holder : hs) {
var value = holder.getter.apply(obj);
for (var rule : holder.rules) {
if (!rule.condition.test(value)) {
throw new ValidateException(rule.errMsg);
}
}
}
}
}
class ValidValueHolder<T, R> {
Function<T, R> getter;
List<RuleInfo<Object>> rules = new ArrayList<>();
public ValidValueHolder(Function<T, R> getter) {
this.getter = getter;
}
private void addRule(Predicate<Object> condition, String errMsg) {
this.rules.add(new RuleInfo<>(condition, errMsg));
}
public ValidValueHolder<T, R> nonNull(String errMsg) {
addRule(Objects::nonNull, errMsg);
return this;
}
public ValidValueHolder<T, R> nonEmpty(String errMsg) {
addRule(value -> {
if (value == null) {
return false;
}
if (value instanceof Collection) {
return ((Collection<?>) value).isEmpty();
}
if (value instanceof String) {
return ((String) value).isEmpty();
}
return false;
}, errMsg);
return this;
}
public ValidValueHolder<T, R> size(int min, int max, String errMsg) {
addRule(value -> {
if (value == null) {
return false;
}
if (value instanceof Collection) {
int size = ((Collection<?>) value).size();
return size >= min && size <= max;
}
return true;
}, errMsg);
return this;
}
public ValidValueHolder<T, R> length(int min, int max, String errMsg) {
addRule(value -> {
if (value == null) {
return false;
}
if (value instanceof String) {
int length = ((String) value).length();
return length >= min && length <= max;
}
return true;
}, errMsg);
return this;
}
public ValidValueHolder<T, R> matches(String regex, String errMsg) {
addRule(input -> RegexUtil.matches(regex, (String) input), errMsg);
return this;
}
public ValidValueHolder<T, R> matchesOr(String[] regexs, String errMsg) {
addRule(input -> RegexUtil.matchesOr((String) input, regexs), errMsg);
return this;
}
public ValidValueHolder<T, R> matchesAnd(String[] regexs, String errMsg) {
addRule(input -> RegexUtil.matchesAnd((String) input, regexs), errMsg);
return this;
}
public ValidValueHolder<T, R> email(String errMsg) {
return matches(RegexConsts.EMAIL, errMsg);
}
@SuppressWarnings("unchecked")
public ValidValueHolder<T, R> and(Predicate<R> condition, String errMsg) {
addRule((Predicate<Object>) condition, errMsg);
return this;
}
@AllArgsConstructor
static final class RuleInfo<R> {
Predicate<R> condition;
String errMsg;
}
}

View File

@@ -0,0 +1,12 @@
package xyz.zhouxy.plusone.validator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DtoValidator {
Class<?> value();
}

View File

@@ -0,0 +1,13 @@
package xyz.zhouxy.plusone.validator;
/**
* 自带校验方法,校验不通过时直接抛异常。
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see ValidateUtil
* @see BaseValidator
*/
public interface IValidateRequired {
void validate();
}

View File

@@ -0,0 +1,42 @@
package xyz.zhouxy.plusone.validator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Aspect
@Component
class ValidateDtosConfig {
final Map<Class<?>, BaseValidator<?>> validatorMap = new ConcurrentHashMap<>();
ValidateDtosConfig(ApplicationContext context) {
Map<String, Object> beans = context.getBeansWithAnnotation(DtoValidator.class);
for (var validator : beans.values()) {
Class<?> targetClass = validator.getClass().getAnnotation(DtoValidator.class).value();
this.validatorMap.put(targetClass, (BaseValidator<?>) validator);
}
}
@Before("@annotation(ValidateDto) && args(dto)")
@SuppressWarnings("unchecked")
public <T> void doValidateDto(T dto) {
BaseValidator<T> validator = (BaseValidator<T>) this.validatorMap.get(dto.getClass());
if (validator != null) {
validator.validate(dto);
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateDto {
}

View File

@@ -0,0 +1,29 @@
package xyz.zhouxy.plusone.validator;
/**
* 校验工具类
* <p>
* 对 {@link IValidateRequired} 的实现类对象进行校验
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see BaseValidator
* @see Validator
* @see IValidateRequired
*/
public class ValidateUtil {
private ValidateUtil() {
throw new IllegalStateException("Utility class");
}
public static void validate(Object obj) {
if (obj instanceof IValidateRequired) {
((IValidateRequired) obj).validate();
}
}
public static <T> void validate(T obj, BaseValidator<T> validator) {
validator.validate(obj);
}
}

View File

@@ -0,0 +1,42 @@
package xyz.zhouxy.plusone.validator;
import java.util.function.Predicate;
/**
* 校验器
*
* <p>
* 可以使用以下方式初始化一个校验器:
* </p>
*
* <pre>
* var validator = new Validator&lt;Integer&gt;()
* .addRule(value -> Objects.nonNull(value), "value 不能为空")
* .addRule(value -> (value >= 0 && value <= 500), "value 应在 [0, 500] 内");
* </pre>
*
* <p>
* 然后通过校验器的 {@link #validate} 方法,或
* {@link ValidateUtil#validate(Object, Validator)} 对指定对象进行校验。
* </p>
*
* <pre>
* validator.validate(666);
* </pre>
*
* <pre>
* ValidateUtil.validate(255, validator);
* </pre>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see IValidateRequired
* @see ValidateUtil
* @see BaseValidator
*/
public final class Validator<T> extends BaseValidator<T> {
public final Validator<T> addRule(final Predicate<T> rule, final String errorMessage) {
ruleFor(rule, errorMessage);
return this;
}
}

View File

@@ -0,0 +1,23 @@
package xyz.zhouxy.plusone.web.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
public class WebCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "OPTIONS", "PUT", "DELETE", "PATCH")
.maxAge(3600);
}
}

View File

@@ -0,0 +1,55 @@
package xyz.zhouxy.plusone.web.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Spring MVC 配置。
*
* <p>
* 将与前后端交互的数据中的枚举转换为其整数值
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
public class WebEnumMapConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new IntToEnumConverterFactory());
registry.addConverterFactory(new StringToEnumConverterFactory());
}
}
class IntToEnumConverterFactory implements ConverterFactory<Integer, Enum<?>> {
@Override
public <T extends Enum<?>> Converter<Integer, T> getConverter(Class<T> targetType) {
return (Integer source) -> {
try {
T[] values = targetType.getEnumConstants();
return values[source];
} catch (IndexOutOfBoundsException e) {
throw new EnumConstantNotPresentException(targetType, Integer.toString(source));
}
};
}
}
class StringToEnumConverterFactory implements ConverterFactory<String, Enum<?>> {
@Override
public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
return (String source) -> {
int index = Integer.parseInt(source);
try {
T[] values = targetType.getEnumConstants();
return values[index];
} catch (IndexOutOfBoundsException e) {
throw new EnumConstantNotPresentException(targetType, Integer.toString(index));
}
};
}
}

View File

@@ -0,0 +1,31 @@
spring.application.name=${plusone.application.name}
spring.mail.host=${plusone.mail.host}
spring.mail.username=${plusone.mail.from}
spring.mail.password=${plusone.mail.password}
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.debug=${plusone.debug}
server.port=${plusone.server.port}
mybatis-plus.global-config.banner=false
# 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
mybatis-plus.global-config.db-config.logic-delete-field=deleted
# 逻辑已删除值(默认为 1)
mybatis-plus.global-config.db-config.logic-delete-value=id
# 逻辑未删除值(默认为 0)
mybatis-plus.global-config.db-config.logic-not-delete-value=0
mybatis-plus.configuration.default-enum-type-handler=org.apache.ibatis.type.EnumOrdinalTypeHandler
# Sa-Token 配置
sa-token.is-print=false
# token名称 (同时也是cookie名称)
sa-token.token-name=${plusone.application.name}
# 是否允许同一账号并发登录 (为true时允许一起登录, 为 false 时新登录挤掉旧登录)
sa-token.is-concurrent=true
# 在多人登录同一账号时是否共用一个token (为 true 时所有登录共用一个 token, 为false时每次登录新建一个 token)
sa-token.is-share=true
sa-token.token-style=simple-uuid
sa-token.activity-timeout=1800
sa-token.is-read-cookie=false
sa-token.is-log=${plusone.debug}