first commit.
This commit is contained in:
@@ -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 {
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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<Integer> validator = new BaseValidator<>() {
|
||||
* {
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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 {
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package xyz.zhouxy.plusone.validator;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* 校验器
|
||||
*
|
||||
* <p>
|
||||
* 可以使用以下方式初始化一个校验器:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* var validator = new Validator<Integer>()
|
||||
* .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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -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}
|
@@ -0,0 +1,41 @@
|
||||
package xyz.zhouxy.plusone.validator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import xyz.zhouxy.plusone.constant.RegexConsts;
|
||||
|
||||
class BaseValidator2Test {
|
||||
|
||||
@Test
|
||||
void testValid() {
|
||||
LoginCommand loginCommand = new LoginCommand("13169053215@qq.com", "8GouTDE53a", false);
|
||||
LoginCommandValidator.INSTANCE.validate(loginCommand);
|
||||
System.err.println(loginCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
class LoginCommand {
|
||||
private String account;
|
||||
private String pwd;
|
||||
private boolean rememberMe;
|
||||
}
|
||||
|
||||
class LoginCommandValidator extends BaseValidator2<LoginCommand> {
|
||||
|
||||
public static final LoginCommandValidator INSTANCE = new LoginCommandValidator();
|
||||
|
||||
private LoginCommandValidator() {
|
||||
ruleFor(loginCommand -> loginCommand.getAccount())
|
||||
.nonNull("邮箱地址不能为空")
|
||||
.matchesOr(new String[] { RegexConsts.EMAIL, RegexConsts.MOBILE_PHONE }, "请输入邮箱地址或手机号");
|
||||
ruleFor(loginCommand -> loginCommand.getPwd())
|
||||
.nonNull("密码不能为空")
|
||||
.matches(RegexConsts.PASSWORD, "密码格式错误");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user