Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feat_test-user

 Conflicts:
	src/test/resources/sql/clean.sql
	src/test/resources/sql/create_tables.sql
This commit is contained in:
YunaiV
2021-04-18 01:26:38 +08:00
345 changed files with 17305 additions and 6482 deletions

View File

@@ -0,0 +1,20 @@
package cn.iocoder.dashboard.common.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Key Value 的键值对
*
* @author 芋道源码
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KeyValue<K, V> {
private K key;
private V value;
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.dashboard.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 通用状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum DefaultBitFieldEnum {
NO(0, ""),
YES(1, "");
/**
* 状态值
*/
private final Integer val;
/**
* 状态名
*/
private final String name;
}

View File

@@ -1,12 +1,13 @@
package cn.iocoder.dashboard.common.exception;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange;
import lombok.Data;
/**
* 错误码对象
*
* 全局错误码,占用 [0, 999]参见 {@link GlobalException}
* 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
* 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
*
* TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
@@ -21,11 +22,11 @@ public class ErrorCode {
/**
* 错误提示
*/
private final String message;
private final String msg;
public ErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
this.msg = message;
}
}

View File

@@ -1,41 +0,0 @@
package cn.iocoder.dashboard.common.exception;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 全局异常 Exception
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GlobalException extends RuntimeException {
/**
* 全局错误码
*
* @see GlobalErrorCodeConstants
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 空构造方法,避免反序列化问题
*/
public GlobalException() {
}
public GlobalException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
public GlobalException(Integer code, String message) {
this.code = code;
this.message = message;
}
}

View File

@@ -30,7 +30,7 @@ public final class ServiceException extends RuntimeException {
public ServiceException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
this.message = errorCode.getMsg();
}
public ServiceException(Integer code, String message) {

View File

@@ -32,6 +32,7 @@ public interface GlobalErrorCodeConstants {
// ========== 自定义错误段 ==========
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");

View File

@@ -2,6 +2,7 @@ package cn.iocoder.dashboard.common.exception.util;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.exception.ServiceException;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,12 +47,12 @@ public class ServiceExceptionUtil {
// ========== 和 ServiceException 的集成 ==========
public static ServiceException exception(ErrorCode errorCode) {
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage());
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
return exception0(errorCode.getCode(), messagePattern);
}
public static ServiceException exception(ErrorCode errorCode, Object... params) {
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage());
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
return exception0(errorCode.getCode(), messagePattern, params);
}
@@ -91,7 +92,8 @@ public class ServiceExceptionUtil {
* @param params 参数
* @return 格式化后的提示
*/
private static String doFormat(int code, String messagePattern, Object... params) {
@VisibleForTesting
public static String doFormat(int code, String messagePattern, Object... params) {
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
int i = 0;
int j;

View File

@@ -1,7 +1,6 @@
package cn.iocoder.dashboard.common.pojo;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.exception.GlobalException;
import cn.iocoder.dashboard.common.exception.ServiceException;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -9,6 +8,7 @@ import lombok.Data;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.Objects;
/**
* 通用返回
@@ -16,7 +16,7 @@ import java.io.Serializable;
* @param <T> 数据泛型
*/
@Data
public final class CommonResult<T> implements Serializable {
public class CommonResult<T> implements Serializable {
/**
* 错误码
@@ -31,7 +31,7 @@ public final class CommonResult<T> implements Serializable {
/**
* 错误提示,用户可阅读
*
* @see ErrorCode#getMessage() ()
* @see ErrorCode#getMsg() ()
*/
private String msg;
@@ -57,7 +57,7 @@ public final class CommonResult<T> implements Serializable {
}
public static <T> CommonResult<T> error(ErrorCode errorCode) {
return error(errorCode.getCode(), errorCode.getMessage());
return error(errorCode.getCode(), errorCode.getMsg());
}
public static <T> CommonResult<T> success(T data) {
@@ -68,9 +68,13 @@ public final class CommonResult<T> implements Serializable {
return result;
}
public static boolean isSuccess(Integer code) {
return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
}
@JsonIgnore // 避免 jackson 序列化
public boolean isSuccess() {
return GlobalErrorCodeConstants.SUCCESS.getCode().equals(code);
return isSuccess(code);
}
@JsonIgnore // 避免 jackson 序列化
@@ -81,16 +85,12 @@ public final class CommonResult<T> implements Serializable {
// ========= 和 Exception 异常体系集成 =========
/**
* 判断是否有异常。如果有,则抛出 {@link GlobalException} 或 {@link ServiceException} 异常
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
*/
public void checkError() throws GlobalException, ServiceException {
public void checkError() throws ServiceException {
if (isSuccess()) {
return;
}
// 全局异常
if (GlobalErrorCodeConstants.isMatch(code)) {
throw new GlobalException(code, msg);
}
// 业务异常
throw new ServiceException(code, msg);
}
@@ -99,8 +99,4 @@ public final class CommonResult<T> implements Serializable {
return error(serviceException.getCode(), serviceException.getMessage());
}
public static <T> CommonResult<T> error(GlobalException globalException) {
return error(globalException.getCode(), globalException.getMessage());
}
}

View File

@@ -13,14 +13,17 @@ import java.io.Serializable;
@Data
public class PageParam implements Serializable {
private static final Integer PAGE_NO = 1;
private static final Integer PAGE_SIZE = 10;
@ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1")
@NotNull(message = "页码不能为空")
@Min(value = 1, message = "页码最小值为 1")
private Integer pageNo;
private Integer pageNo = PAGE_NO;
@ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10")
@NotNull(message = "每页条数不能为空")
@Range(min = 1, max = 100, message = "条数范围为 [1, 100]")
private Integer pageSize;
private Integer pageSize = PAGE_SIZE;
}

View File

@@ -13,6 +13,11 @@ import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
/**
* Excel {@link SysDictDataDO} 数据字典转换器
*
* @author 芋道源码
*/
@Slf4j
public class DictConvert implements Converter<Object> {

View File

@@ -0,0 +1,39 @@
package cn.iocoder.dashboard.framework.excel.core.convert;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* Excel Json 转换器
*
* @author 芋道源码
*/
public class JsonConvert implements Converter<Object> {
@Override
public Class<?> supportJavaTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellData<String> convertToExcelData(Object value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 生成 Excel 小表格
return new CellData<>(JsonUtils.toJsonString(value));
}
}

View File

@@ -1,6 +1,6 @@
package cn.iocoder.dashboard.framework.file.config;
import cn.iocoder.dashboard.modules.system.controller.common.SysFileController;
import cn.iocoder.dashboard.modules.infra.controller.file.InfFileController;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@@ -13,7 +13,7 @@ import javax.validation.constraints.NotNull;
public class FileProperties {
/**
* 对应 {@link SysFileController#}
* 对应 {@link InfFileController#}
*/
@NotNull(message = "基础文件路径不能为空")
private String basePath;

View File

@@ -1,18 +1,35 @@
package cn.iocoder.dashboard.framework.jackson.config;
import cn.iocoder.dashboard.framework.jackson.deser.LocalDateTimeDeserializer;
import cn.iocoder.dashboard.framework.jackson.ser.LocalDateTimeSerializer;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
@Configuration
public class JacksonConfig {
@Bean
@SuppressWarnings("InstantiationOfUtilityClass")
public JsonUtils jsonUtils(ObjectMapper objectMapper) {
SimpleModule simpleModule = new SimpleModule();
/*
* 1. 新增Long类型序列化规则数值超过2^53-1在JS会出现精度丢失问题因此Long自动序列化为字符串类型
* 2. 新增LocalDateTime序列化、反序列化规则
*/
simpleModule
// .addSerializer(Long.class, ToStringSerializer.instance)
// .addSerializer(Long.TYPE, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE)
.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
objectMapper.registerModules(simpleModule);
JsonUtils.init(objectMapper);
return new JsonUtils();
}
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.dashboard.framework.jackson.deser;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* LocalDateTime反序列化规则
* <p>
* 会将毫秒级时间戳反序列化为LocalDateTime
*/
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer();
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
}
}

View File

@@ -0,0 +1,24 @@
package cn.iocoder.dashboard.framework.jackson.ser;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* LocalDateTime序列化规则
* <p>
* 会将LocalDateTime序列化为毫秒级时间戳
*/
public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer();
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
}

View File

@@ -81,7 +81,7 @@ public class ApiAccessLogFilter extends OncePerRequestFilter {
Map<String, String> queryString, String requestBody, Exception ex) {
// 处理用户信息
accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
accessLog.setUserType(WebFrameworkUtils.getUesrType(request));
accessLog.setUserType(WebFrameworkUtils.getUserType(request));
// 设置访问结果
CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
if (result != null) {

View File

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.framework.logger.apilog.core.service;
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
import javax.validation.Valid;
import java.util.concurrent.Future;
/**
* API 访问日志 Framework Service 接口
@@ -15,7 +16,8 @@ public interface ApiAccessLogFrameworkService {
* 创建 API 访问日志
*
* @param createDTO 创建信息
* @return 是否创建成功
*/
void createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
Future<Boolean> createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
}

View File

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.framework.logger.apilog.core.service;
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO;
import javax.validation.Valid;
import java.util.concurrent.Future;
/**
* API 错误日志 Framework Service 接口
@@ -15,7 +16,8 @@ public interface ApiErrorLogFrameworkService {
* 创建 API 错误日志
*
* @param createDTO 创建信息
* @return 是否创建成功
*/
void createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
Future<Boolean> createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
}

View File

@@ -2,13 +2,16 @@ package cn.iocoder.dashboard.framework.logger.operatelog.core.service;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO;
import java.util.concurrent.Future;
public interface OperateLogFrameworkService {
/**
* 要不记录操作日志
* 异步记录操作日志
*
* @param reqVO 操作日志请求
* @return true: 记录成功,false: 记录失败
*/
void createOperateLogAsync(SysOperateLogCreateReqVO reqVO);
Future<Boolean> createOperateLogAsync(SysOperateLogCreateReqVO reqVO);
}

View File

@@ -1,5 +1,7 @@
package cn.iocoder.dashboard.framework.mybatis.config;
import cn.iocoder.dashboard.framework.mybatis.core.handler.DefaultDBFieldHandler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.annotations.Mapper;
@@ -13,7 +15,8 @@ import org.springframework.context.annotation.Configuration;
* @author 芋道源码
*/
@Configuration
@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class)
@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class,
lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
public class MybatisConfiguration {
@Bean
@@ -23,4 +26,9 @@ public class MybatisConfiguration {
return mybatisPlusInterceptor;
}
@Bean
public MetaObjectHandler defaultMetaObjectHandler(){
return new DefaultDBFieldHandler(); // 自动填充参数类
}
}

View File

@@ -1,5 +1,7 @@
package cn.iocoder.dashboard.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
@@ -15,19 +17,27 @@ public class BaseDO implements Serializable {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 创建者 TODO 芋艿:迁移成编号
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
private String createBy;
@TableField(fill = FieldFill.INSERT)
private String creator;
/**
* 更新者 TODO 芋艿:迁移成编号
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updater;
/**
* 是否删除
*/

View File

@@ -0,0 +1,63 @@
package cn.iocoder.dashboard.framework.mybatis.core.handler;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import java.util.Date;
import java.util.Objects;
/**
* 通用参数填充实现类
*
* 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
*
* @author hexiaowu
*/
public class DefaultDBFieldHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
Date current = new Date();
// 创建时间为空,则以当前时间为插入时间
if (Objects.isNull(baseDO.getCreateTime())) {
baseDO.setCreateTime(current);
}
// 更新时间为空,则以当前时间为更新时间
if (Objects.isNull(baseDO.getUpdateTime())) {
baseDO.setUpdateTime(current);
}
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人
if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(loginUser.getId().toString());
}
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(loginUser.getId().toString());
}
}
}
@Override
public void updateFill(MetaObject metaObject) {
Object modifyTime = getFieldValByName("updateTime", metaObject);
Object modifier = getFieldValByName("updater", metaObject);
// 获取登录用户信息
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
// 更新时间为空,则以当前时间为更新时间
if (Objects.isNull(modifyTime)) {
setFieldValByName("updateTime", new Date(), metaObject);
}
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) {
setFieldValByName("updater", loginUser.getId().toString(), metaObject);
}
}
}

View File

@@ -24,12 +24,16 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default List<T> selectList() {
return selectList(new QueryWrapper<>());
}
default T selectOne(String field, Object value) {
return selectOne(new QueryWrapper<T>().eq(field, value));
}
default Integer selectCount(String field, Object value) {
return selectCount(new QueryWrapper<T>().eq(field, value));
}
default List<T> selectList() {
return selectList(new QueryWrapper<>());
}
}

View File

@@ -1,14 +1,21 @@
package cn.iocoder.dashboard.framework.redis.config;
import cn.hutool.system.SystemUtil;
import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import java.util.List;
@@ -19,6 +26,9 @@ import java.util.List;
@Slf4j
public class RedisConfig {
/**
* 创建 RedisTemplate Bean使用 JSON 序列化方式
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 创建 RedisTemplate 对象
@@ -27,14 +37,19 @@ public class RedisConfig {
template.setConnectionFactory(factory);
// 使用 String 序列化方式,序列化 KEY 。
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
return template;
}
/**
* 创建 Redis Pub/Sub 广播消费的容器
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory,
List<AbstractChannelMessageListener<?>> listeners) {
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory factory, List<AbstractChannelMessageListener<?>> listeners) {
// 创建 RedisMessageListenerContainer 对象
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 设置 RedisConnection 工厂。
@@ -48,4 +63,57 @@ public class RedisConfig {
return container;
}
/**
* 创建 Redis Stream 集群消费的容器
*
* Redis Stream 的 xreadgroup 命令https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
*/
@Bean(initMethod = "start", destroyMethod = "stop")
public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
RedisTemplate<String, Object> redisTemplate, List<AbstractStreamMessageListener<?>> listeners) {
// 第一步,创建 StreamMessageListenerContainer 容器
// 创建 options 配置
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
.batchSize(10) // 一次性最多拉取多少条消息
.targetType(String.class) // 目标类型。统一使用 String通过自己封装的 AbstractStreamMessageListener 去反序列化
.build();
// 创建 container 对象
StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer.create(
redisTemplate.getRequiredConnectionFactory(), containerOptions);
// 第二步,注册监听器,消费对应的 Stream 主题
String consumerName = buildConsumerName();
// String consumerName = "110";
listeners.forEach(listener -> {
// 创建 listener 对应的消费者分组
try {
redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
} catch (Exception ignore) {}
// 设置 listener 对应的 redisTemplate
listener.setRedisTemplate(redisTemplate);
// 创建 Consumer 对象
Consumer consumer = Consumer.from(listener.getGroup(), consumerName);
// 设置 Consumer 消费进度,以最小消费进度为准
StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed());
// 设置 Consumer 监听
StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
.builder(streamOffset).consumer(consumer)
.autoAcknowledge(false) // 不自动 ack
.cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
container.register(builder.build(), listener);
});
return container;
}
/**
* 构建消费者名字,使用本地 IP + 进程编号的方式。
* 参考自 RocketMQ clientId 的实现
*
* @return 消费者名字
*/
private static String buildConsumerName() {
return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
}
}

View File

@@ -1,11 +1,10 @@
package cn.iocoder.dashboard.framework.redis.core.pubsub;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.TypeUtil;
import cn.iocoder.dashboard.util.json.JsonUtils;
import lombok.SneakyThrows;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import java.lang.reflect.Type;
@@ -62,21 +61,11 @@ public abstract class AbstractChannelMessageListener<T extends ChannelMessage> i
*/
@SuppressWarnings("unchecked")
private Class<T> getMessageClass() {
Class<?> targetClass = getClass();
while (targetClass.getSuperclass() != null) {
// 如果不是 AbstractMessageListener 父类,继续向上查找
if (targetClass.getSuperclass() != AbstractChannelMessageListener.class) {
targetClass = targetClass.getSuperclass();
continue;
}
// 如果是 AbstractMessageListener 父类,则解析泛型
Type[] types = ((ParameterizedTypeImpl) targetClass.getGenericSuperclass()).getActualTypeArguments();
if (ArrayUtil.isEmpty(types)) {
throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
}
return (Class<T>) types[0];
Type type = TypeUtil.getTypeArgument(getClass(), 0);
if (type == null) {
throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
}
throw new IllegalStateException(String.format("类型(%s) 找不到 AbstractMessageListener 父类", getClass().getName()));
return (Class<T>) type;
}
}

View File

@@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* Redis Channel Message 接口
*
* @author 芋道源码
*/
public interface ChannelMessage {
@@ -12,7 +14,7 @@ public interface ChannelMessage {
*
* @return Channel
*/
@JsonIgnore // 必须序列化
@JsonIgnore // 避免序列化
String getChannel();
}

View File

@@ -0,0 +1,88 @@
package cn.iocoder.dashboard.framework.redis.core.stream;
import cn.hutool.core.util.TypeUtil;
import cn.iocoder.dashboard.util.json.JsonUtils;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.stream.StreamListener;
import java.lang.reflect.Type;
/**
* Redis Stream 监听器抽象类,用于实现集群消费
*
* @param <T> 消息类型。一定要填写噢,不然会报错
*
* @author 芋道源码
*/
public abstract class AbstractStreamMessageListener<T extends StreamMessage>
implements StreamListener<String, ObjectRecord<String, String>> {
/**
* 消息类型
*/
private final Class<T> messageType;
/**
* Redis Channel
*/
@Getter
private final String streamKey;
/**
* Redis 消费者分组,默认使用 spring.application.name 名字
*/
@Value("${spring.application.name}")
@Getter
private String group;
/**
*
*/
@Setter
private RedisTemplate<String, ?> redisTemplate;
@SneakyThrows
protected AbstractStreamMessageListener() {
this.messageType = getMessageClass();
this.streamKey = messageType.newInstance().getStreamKey();
}
@Override
public void onMessage(ObjectRecord<String, String> message) {
// 消费消息
T messageObj = JsonUtils.parseObject(message.getValue(), messageType);
this.onMessage(messageObj);
// ack 消息消费完成
redisTemplate.opsForStream().acknowledge(group, message);
// TODO 芋艿:需要额外考虑以下几个点:
// 1. 处理异常的情况
// 2. 发送日志;以及事务的结合
// 3. 消费日志;以及通用的幂等性
// 4. 消费失败的重试https://zhuanlan.zhihu.com/p/60501638
}
/**
* 处理消息
*
* @param message 消息
*/
public abstract void onMessage(T message);
/**
* 通过解析类上的泛型,获得消息类型
*
* @return 消息类型
*/
@SuppressWarnings("unchecked")
private Class<T> getMessageClass() {
Type type = TypeUtil.getTypeArgument(getClass(), 0);
if (type == null) {
throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
}
return (Class<T>) type;
}
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.dashboard.framework.redis.core.stream;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* Redis Stream Message 接口
*
* @author 芋道源码
*/
public interface StreamMessage {
/**
* 获得 Redis Stream Key
*
* @return Channel
*/
@JsonIgnore // 避免序列化
String getStreamKey();
}

View File

@@ -1,7 +1,10 @@
package cn.iocoder.dashboard.framework.redis.core.util;
import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage;
import cn.iocoder.dashboard.util.json.JsonUtils;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.core.RedisTemplate;
/**
@@ -17,8 +20,21 @@ public class RedisMessageUtils {
* @param redisTemplate Redis 操作模板
* @param message 消息
*/
public static <T extends ChannelMessage> void sendChannelMessage(RedisTemplate<?, ?> redisTemplate, T message) {
public static <T extends ChannelMessage> void sendChannelMessage(RedisTemplate<?, ?> redisTemplate, T message) {
redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
}
/**
* 发送 Redis 消息,基于 Redis Stream 实现
*
* @param redisTemplate Redis 操作模板
* @param message 消息
* @return 消息记录的编号对象
*/
public static <T extends StreamMessage> RecordId sendStreamMessage(RedisTemplate<String, ?> redisTemplate, T message) {
return redisTemplate.opsForStream().add(StreamRecords.newRecord()
.ofObject(JsonUtils.toJsonString(message)) // 设置内容
.withStreamKey(message.getStreamKey())); // 设置 stream key
}
}

View File

@@ -1 +0,0 @@
<http://www.iocoder.cn/Spring-Boot/Spring-Security/?github>

View File

@@ -0,0 +1 @@
<https://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao>

View File

@@ -128,13 +128,13 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 设置每个请求的权限
.authorizeRequests()
// 登陆的接口,可匿名访问
.antMatchers(webProperties.getApiPrefix() + "/login").anonymous()
.antMatchers(api("/login")).anonymous()
// 通用的接口,可匿名访问
.antMatchers( webProperties.getApiPrefix() + "/system/captcha/**").anonymous()
.antMatchers(api("/system/captcha/**")).anonymous()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
// 文件的获取接口,可匿名访问
.antMatchers(webProperties.getApiPrefix() + "/system/file/get/**").anonymous()
.antMatchers(api("/infra/file/get/**")).anonymous()
// Swagger 接口文档
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
@@ -148,13 +148,19 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
.antMatchers("/actuator/**").anonymous()
// Druid 监控
.antMatchers("/druid/**").anonymous()
// 短信回调 API
.antMatchers(api("/system/sms/callback/**")).anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
httpSecurity.logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler);
// 添加 JWT Filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
private String api(String url) {
return webProperties.getApiPrefix() + url;
}
}

View File

@@ -1,6 +1,7 @@
package cn.iocoder.dashboard.framework.security.core.handler;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
@@ -36,6 +37,6 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
securityFrameworkService.logout(token);
}
// 返回成功
ServletUtils.writeJSON(response, null);
ServletUtils.writeJSON(response, CommonResult.success(null));
}
}

View File

@@ -2,7 +2,10 @@ package cn.iocoder.dashboard.framework.security.core.util;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
@@ -40,9 +43,20 @@ public class SecurityFrameworkUtils {
/**
* 获取当前用户
*
* @return 当前用户
*/
@Nullable
public static LoginUser getLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SecurityContext context = SecurityContextHolder.getContext();
if (context == null) {
return null;
}
Authentication authentication = context.getAuthentication();
if (authentication == null) {
return null;
}
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
}
/**
@@ -50,8 +64,10 @@ public class SecurityFrameworkUtils {
*
* @return 用户编号
*/
@Nullable
public static Long getLoginUserId() {
return getLoginUser().getId();
LoginUser loginUser = getLoginUser();
return loginUser != null ? loginUser.getId() : null;
}
/**
@@ -59,8 +75,10 @@ public class SecurityFrameworkUtils {
*
* @return 角色编号数组
*/
@Nullable
public static Set<Long> getLoginUserRoleIds() {
return getLoginUser().getRoleIds();
LoginUser loginUser = getLoginUser();
return loginUser != null ? loginUser.getRoleIds() : null;
}
/**

View File

@@ -0,0 +1,21 @@
package cn.iocoder.dashboard.framework.sms.config;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.dashboard.framework.sms.core.client.impl.SmsClientFactoryImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 短信配置类
*
* @author 芋道源码
*/
@Configuration
public class SmsConfiguration {
@Bean
public SmsClientFactory smsClientFactory() {
return new SmsClientFactoryImpl();
}
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import java.util.List;
/**
* 短信客户端接口
*
* @author zzf
* @date 2021/1/25 14:14
*/
public interface SmsClient {
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long getId();
/**
* 发送消息
*
* @param logId 日志编号
* @param mobile 手机号
* @param apiTemplateId 短信 API 的模板编号
* @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序
* @return 短信发送结果
*/
SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile, String apiTemplateId,
List<KeyValue<String, Object>> templateParams);
/**
* 解析接收短信的接收结果
*
* @param text 结果
* @return 结果内容
* @throws Throwable 当解析 text 发生异常时,则会抛出异常
*/
List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable;
/**
* 查询指定的短信模板
*
* @param apiTemplateId 短信 API 的模板编号
* @return 短信模板
*/
SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId);
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
/**
* 短信客户端工厂接口
*
* @author zzf
* @date 2021/1/28 14:01
*/
public interface SmsClientFactory {
/**
* 获得短信 Client
*
* @param channelId 渠道编号
* @return 短信 Client
*/
SmsClient getSmsClient(Long channelId);
/**
* 获得短信 Client
*
* @param channelCode 渠道编码
* @return 短信 Client
*/
SmsClient getSmsClient(String channelCode);
/**
* 创建短信 Client
*
* @param properties 配置对象
*/
void createOrUpdateSmsClient(SmsChannelProperties properties);
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import java.util.function.Function;
/**
* 将 API 的错误码,转换为通用的错误码
*
* @see SmsCommonResult
* @see SmsFrameworkErrorCodeConstants
*
* @author 芋道源码
*/
public interface SmsCodeMapping extends Function<String, ErrorCode> {
}

View File

@@ -0,0 +1,68 @@
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 短信的 CommonResult 拓展类
*
* 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
*
* 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SmsCommonResult<T> extends CommonResult<T> {
/**
* API 返回错误码
*
* 由于第三方的错误码可能是字符串,所以使用 String 类型
*/
private String apiCode;
/**
* API 返回提示
*/
private String apiMsg;
/**
* API 请求编号
*/
private String apiRequestId;
private SmsCommonResult() {
}
public static <T> SmsCommonResult<T> build(String apiCode, String apiMsg, String apiRequestId,
T data, SmsCodeMapping codeMapping) {
Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
SmsCommonResult<T> result = new SmsCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId);
result.setData(data);
// 翻译错误码
if (codeMapping != null) {
ErrorCode errorCode = codeMapping.apply(apiCode);
if (errorCode == null) {
errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
}
result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
}
return result;
}
public static <T> SmsCommonResult<T> error(Throwable ex) {
SmsCommonResult<T> result = new SmsCommonResult<>();
result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode());
result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
return result;
}
}

View File

@@ -0,0 +1,48 @@
package cn.iocoder.dashboard.framework.sms.core.client.dto;
import lombok.Data;
import java.util.Date;
/**
* 消息接收 Response DTO
*
* @author 芋道源码
*/
@Data
public class SmsReceiveRespDTO {
/**
* 是否接收成功
*/
private Boolean success;
/**
* API 接收结果的编码
*/
private String errorCode;
/**
* API 接收结果的说明
*/
private String errorMsg;
/**
* 手机号
*/
private String mobile;
/**
* 用户接收时间
*/
private Date receiveTime;
/**
* 短信 API 发送返回的序号
*/
private String serialNo;
/**
* 短信日志编号
*
* 对应 SysSmsLogDO 的编号
*/
private Long logId;
}

View File

@@ -0,0 +1,18 @@
package cn.iocoder.dashboard.framework.sms.core.client.dto;
import lombok.Data;
/**
* 短信发送 Response DTO
*
* @author 芋道源码
*/
@Data
public class SmsSendRespDTO {
/**
* 短信 API 发送返回的序号
*/
private String serialNo;
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.dashboard.framework.sms.core.client.dto;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import lombok.Data;
/**
* 短信模板 Response DTO
*
* @author 芋道源码
*/
@Data
public class SmsTemplateRespDTO {
/**
* 模板编号
*/
private String id;
/**
* 短信内容
*/
private String content;
/**
* 审核状态
*
* 枚举 {@link SmsTemplateAuditStatusEnum}
*/
private Integer auditStatus;
/**
* 审核未通过的理由
*/
private String auditReason;
}

View File

@@ -0,0 +1,122 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 短信客户端抽象类
*
* @author zzf
* @date 2021/2/1 9:28
*/
@Slf4j
public abstract class AbstractSmsClient implements SmsClient {
/**
* 短信渠道配置
*/
protected volatile SmsChannelProperties properties;
/**
* 错误码枚举类
*/
protected final SmsCodeMapping codeMapping;
/**
* 短信客户端有参构造函数
*
* @param properties 短信配置
*/
public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
this.properties = properties;
this.codeMapping = codeMapping;
}
/**
* 初始化
*/
public final void init() {
doInit();
log.info("[init][配置({}) 初始化完成]", properties);
}
public final void refresh(SmsChannelProperties properties) {
// 判断是否更新
if (properties.equals(this.properties)) {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", properties);
this.properties = properties;
// 初始化
this.init();
}
/**
* 自定义初始化
*/
protected abstract void doInit();
@Override
public Long getId() {
return properties.getId();
}
@Override
public final SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
// 执行短信发送
SmsCommonResult<SmsSendRespDTO> result;
try {
result = doSendSms(logId, mobile, apiTemplateId, templateParams);
} catch (Throwable ex) {
// 打印异常日志
log.error("[sendSms][发送短信异常sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]",
logId, mobile, apiTemplateId, templateParams, ex);
// 封装返回
return SmsCommonResult.error(ex);
}
return result;
}
protected abstract SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams)
throws Throwable;
@Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
try {
return doParseSmsReceiveStatus(text);
} catch (Throwable ex) {
log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex);
throw ex;
}
}
protected abstract List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable;
@Override
public SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId) {
// 执行短信发送
SmsCommonResult<SmsTemplateRespDTO> result;
try {
result = doGetSmsTemplate(apiTemplateId);
} catch (Throwable ex) {
// 打印异常日志
log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex);
// 封装返回
return SmsCommonResult.error(ex);
}
return result;
}
protected abstract SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable;
}

View File

@@ -0,0 +1,90 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
import cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian.YunpianSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 短信客户端工厂接口
*
* @author zzf
*/
@Validated
@Slf4j
public class SmsClientFactoryImpl implements SmsClientFactory {
/**
* 短信客户端 Map
* key渠道编号使用 {@link SmsChannelProperties#getId()}
*/
private final ConcurrentMap<Long, AbstractSmsClient> channelIdClients = new ConcurrentHashMap<>();
/**
* 短信客户端 Map
* key渠道编码使用 {@link SmsChannelProperties#getCode()} ()}
*
* 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。
* 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients}
*/
private final ConcurrentMap<String, AbstractSmsClient> channelCodeClients = new ConcurrentHashMap<>();
public SmsClientFactoryImpl() {
// 初始化 channelCodeClients 集合
Arrays.stream(SmsChannelEnum.values()).forEach(channel -> {
// 创建一个空的 SmsChannelProperties 对象
SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode())
.setApiKey("default").setApiSecret("default");
// 创建 Sms 客户端
AbstractSmsClient smsClient = createSmsClient(properties);
channelCodeClients.put(channel.getCode(), smsClient);
});
}
@Override
public SmsClient getSmsClient(Long channelId) {
return channelIdClients.get(channelId);
}
@Override
public SmsClient getSmsClient(String channelCode) {
return channelCodeClients.get(channelCode);
}
@Override
public void createOrUpdateSmsClient(SmsChannelProperties properties) {
AbstractSmsClient client = channelIdClients.get(properties.getId());
if (client == null) {
client = this.createSmsClient(properties);
client.init();
channelIdClients.put(client.getId(), client);
} else {
client.refresh(properties);
}
}
private AbstractSmsClient createSmsClient(SmsChannelProperties properties) {
SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode());
Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum));
// 创建客户端
switch (channelEnum) {
case ALIYUN: return new AliyunSmsClient(properties);
case YUN_PIAN: return new YunpianSmsClient(properties);
case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties));
}
}

View File

@@ -0,0 +1,212 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.util.collection.MapUtils;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.aliyuncs.AcsRequest;
import com.aliyuncs.AcsResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 阿里短信客户端的实现类
*
* @author zzf
* @date 2021/1/25 14:17
*/
@Slf4j
public class AliyunSmsClient extends AbstractSmsClient {
/**
* REGION, 使用杭州
*/
private static final String ENDPOINT = "cn-hangzhou";
/**
* 阿里云客户端
*/
private volatile IAcsClient client;
public AliyunSmsClient(SmsChannelProperties properties) {
super(properties, new AliyunSmsCodeMapping());
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
@Override
protected void doInit() {
IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret());
client = new DefaultAcsClient(profile);
}
@Override
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
// 构建参数
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(mobile);
request.setSignName(properties.getSignature());
request.setTemplateCode(apiTemplateId);
request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
request.setOutId(String.valueOf(sendLogId));
// 执行请求
return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId()));
}
@Override
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
return statuses.stream().map(status -> {
SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
resp.setSuccess(status.getSuccess());
resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg());
resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime());
resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()));
return resp;
}).collect(Collectors.toList());
}
@Override
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
// 构建参数
QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
request.setTemplateCode(apiTemplateId);
// 执行请求
return invoke(request, response -> {
SmsTemplateRespDTO data = new SmsTemplateRespDTO();
data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
return data;
});
}
@VisibleForTesting
Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
switch (templateStatus) {
case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
}
}
@VisibleForTesting
<T extends AcsResponse, R> SmsCommonResult<R> invoke(AcsRequest<T> request, Function<T, R> responseConsumer) {
try {
// 执行发送. 由于阿里云 sms 短信没有统一的 Response但是有统一的 code、message、requestId 属性,所以只好反射
T sendResult = client.getAcsResponse(request);
String code = (String) ReflectUtil.getFieldValue(sendResult, "code");
String message = (String) ReflectUtil.getFieldValue(sendResult, "message");
String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId");
// 解析结果
R data = null;
if (Objects.equals(code, "OK")) { // 请求成功的情况下
data = responseConsumer.apply(sendResult);
}
// 拼接结果
return SmsCommonResult.build(code, message, requestId, data, codeMapping);
} catch (ClientException ex) {
return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping);
}
}
private static String formatResultMsg(ClientException ex) {
if (StrUtil.isEmpty(ex.getErrorDescription())) {
return ex.getErrMsg();
}
return ex.getErrMsg() + " => " + ex.getErrorDescription();
}
/**
* 短信接收状态
*
* 参见 https://help.aliyun.com/document_detail/101867.html 文档
*
* @author 芋道源码
*/
@Data
public static class SmsReceiveStatus {
/**
* 手机号
*/
@JsonProperty("phone_number")
private String phoneNumber;
/**
* 发送时间
*/
@JsonProperty("send_time")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private Date sendTime;
/**
* 状态报告时间
*/
@JsonProperty("report_time")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private Date reportTime;
/**
* 是否接收成功
*/
private Boolean success;
/**
* 状态报告说明
*/
@JsonProperty("err_msg")
private String errMsg;
/**
* 状态报告编码
*/
@JsonProperty("err_code")
private String errCode;
/**
* 发送序列号
*/
@JsonProperty("biz_id")
private String bizId;
/**
* 用户序列号
*
* 这里我们传递的是 SysSmsLogDO 的日志编号
*/
@JsonProperty("out_id")
private String outId;
/**
* 短信长度,例如说 1、2、3
*
* 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送
*/
@JsonProperty("sms_size")
private Integer smsSize;
}
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
/**
* 阿里云的 SmsCodeMapping 实现类
*
* 参见 https://help.aliyun.com/document_detail/101346.htm 文档
*
* @author 芋道源码
*/
public class AliyunSmsCodeMapping implements SmsCodeMapping {
@Override
public ErrorCode apply(String apiCode) {
switch (apiCode) {
case "OK": return GlobalErrorCodeConstants.SUCCESS;
case "isv.ACCOUNT_NOT_EXISTS":
case "isv.ACCOUNT_ABNORMAL":
case "MissingAccessKeyId": return SMS_ACCOUNT_INVALID;
case "isp.RAM_PERMISSION_DENY": return SMS_PERMISSION_DENY;
case "isv.INVALID_JSON_PARAM":
case "isv.INVALID_PARAMETERS": return SMS_API_PARAM_ERROR;
case "isv.BUSINESS_LIMIT_CONTROL": return SMS_SEND_BUSINESS_LIMIT_CONTROL;
case "isv.DAY_LIMIT_CONTROL": return SMS_SEND_DAY_LIMIT_CONTROL;
case "isv.SMS_CONTENT_ILLEGAL": return SMS_SEND_CONTENT_INVALID;
case "isv.SMS_TEMPLATE_ILLEGAL": return SMS_TEMPLATE_INVALID;
case "isv.SMS_SIGNATURE_ILLEGAL":
case "isv.SIGN_NAME_ILLEGAL":
case "isv.SMS_SIGN_ILLEGAL": return SMS_SIGN_INVALID;
case "isv.AMOUNT_NOT_ENOUGH":
case "isv.OUT_OF_SERVICE": return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
case "isv.MOBILE_NUMBER_ILLEGAL": return SMS_MOBILE_INVALID;
case "isv.TEMPLATE_MISSING_PARAMETERS": return SMS_TEMPLATE_PARAM_ERROR;
}
return SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,23 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
import java.util.Objects;
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
/**
* 钉钉的 SmsCodeMapping 实现类
*
* @author 芋道源码
*/
public class DebugDingTalkCodeMapping implements SmsCodeMapping {
@Override
public ErrorCode apply(String apiCode) {
return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,96 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.http.HttpUtil;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.util.collection.MapUtils;
import cn.iocoder.dashboard.util.json.JsonUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 基于钉钉 WebHook 实现的调试的短信客户端实现类
*
* 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。
*
* @author 芋道源码
*/
public class DebugDingTalkSmsClient extends AbstractSmsClient {
public DebugDingTalkSmsClient(SmsChannelProperties properties) {
super(properties, new DebugDingTalkCodeMapping());
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
@Override
protected void doInit() {
}
@Override
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
// 构建请求
String url = buildUrl("robot/send");
Map<String, Object> params = new HashMap<>();
params.put("msgtype", "text");
String content = String.format("【模拟短信】\n手机号%s\n短信日志编号%d\n模板参数%s",
mobile, sendLogId, MapUtils.convertMap(templateParams));
params.put("text", MapUtil.builder().put("content", content).build());
// 执行请求
String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params));
// 解析结果
Map<?, ?> responseObj = JsonUtils.parseObject(responseText, Map.class);
return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"),
null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping);
}
/**
* 构建请求地址
*
* 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档
*
* @param path 请求路径
* @return 请求地址
*/
@SuppressWarnings("SameParameterValue")
private String buildUrl(String path) {
// 生成 timestamp
long timestamp = System.currentTimeMillis();
// 生成 sign
String secret = properties.getApiSecret();
String stringToSign = timestamp + "\n" + secret;
byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign);
String sign = Base64.encode(signData);
// 构建最终 URL
return String.format("https://oapi.dingtalk.com/%s?access_token=%s&timestamp=%d&sign=%s",
path, properties.getApiKey(), timestamp, sign);
}
@Override
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调");
}
@Override
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason("");
return SmsCommonResult.build("0", "success", null, data, codeMapping);
}
}

View File

@@ -0,0 +1,204 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.iocoder.dashboard.common.core.KeyValue;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.yunpian.sdk.YunpianClient;
import com.yunpian.sdk.constant.YunpianConstant;
import com.yunpian.sdk.model.Result;
import com.yunpian.sdk.model.Template;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 云片短信客户端的实现类
*
* @author zzf
* @date 9:48 2021/3/5
*/
@Slf4j
public class YunpianSmsClient extends AbstractSmsClient {
/**
* 云信短信客户端
*/
private volatile YunpianClient client;
public YunpianSmsClient(SmsChannelProperties properties) {
super(properties, new YunpianSmsCodeMapping());
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
}
@Override
public void doInit() {
YunpianClient oldClient = client;
// 初始化新的客户端
YunpianClient newClient = new YunpianClient(properties.getApiKey());
newClient.init();
this.client = newClient;
// 销毁老的客户端
if (oldClient != null) {
oldClient.close();
}
}
@Override
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
return invoke(() -> {
Map<String, String> request = new HashMap<>();
request.put(YunpianConstant.MOBILE, mobile);
request.put(YunpianConstant.TPL_ID, apiTemplateId);
request.put(YunpianConstant.TPL_VALUE, formatTplValue(templateParams));
request.put(YunpianConstant.UID, String.valueOf(sendLogId));
request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl());
return client.sms().tpl_single_send(request);
}, response -> new SmsSendRespDTO().setSerialNo(String.valueOf(response.getSid())));
}
private static String formatTplValue(List<KeyValue<String, Object>> templateParams) {
if (CollUtil.isEmpty(templateParams)) {
return "";
}
// 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化
StringJoiner joiner = new StringJoiner("&");
templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), URLUtil.encode(String.valueOf(param.getValue())))));
return joiner.toString();
}
@Override
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
return statuses.stream().map(status -> {
SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
resp.setSuccess(Objects.equals(status.getReportStatus(), "SUCCESS"));
resp.setErrorCode(status.getErrorMsg()).setErrorMsg(status.getErrorDetail());
resp.setMobile(status.getMobile()).setReceiveTime(status.getUserReceiveTime());
resp.setSerialNo(String.valueOf(status.getSid())).setLogId(status.getUid());
return resp;
}).collect(Collectors.toList());
}
@Override
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable {
return invoke(() -> {
Map<String, String> request = new HashMap<>();
request.put(YunpianConstant.APIKEY, properties.getApiKey());
request.put(YunpianConstant.TPL_ID, apiTemplateId);
return client.tpl().get(request);
}, response -> {
Template template = response.get(0);
return new SmsTemplateRespDTO().setId(String.valueOf(template.getTpl_id())).setContent(template.getTpl_content())
.setAuditStatus(convertSmsTemplateAuditStatus(template.getCheck_status())).setAuditReason(template.getReason());
});
}
@VisibleForTesting
Integer convertSmsTemplateAuditStatus(String checkStatus) {
switch (checkStatus) {
case "CHECKING": return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
case "SUCCESS": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
case "FAIL": return SmsTemplateAuditStatusEnum.FAIL.getStatus();
default: throw new IllegalArgumentException(String.format("未知审核状态(%s)", checkStatus));
}
}
@VisibleForTesting
<T, R> SmsCommonResult<R> invoke(Supplier<Result<T>> requestConsumer, Function<T, R> responseConsumer) throws Throwable {
// 执行请求
Result<T> result = requestConsumer.get();
if (result.getThrowable() != null) {
throw result.getThrowable();
}
// 解析结果
R data = null;
if (result.getData() != null) {
data = responseConsumer.apply(result.getData());
}
// 拼接结果
return SmsCommonResult.build(String.valueOf(result.getCode()), formatResultMsg(result), null, data, codeMapping);
}
private static String formatResultMsg(Result<?> sendResult) {
if (StrUtil.isEmpty(sendResult.getDetail())) {
return sendResult.getMsg();
}
return sendResult.getMsg() + " => " + sendResult.getDetail();
}
/**
* 短信接收状态
*
* 参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档
*
* @author 芋道源码
*/
@Data
public static class SmsReceiveStatus {
/**
* 接收状态
*
* 目前仅有 SUCCESS / FAIL所以使用 Boolean 接收
*/
@JsonProperty("report_status")
private String reportStatus;
/**
* 接收手机号
*/
private String mobile;
/**
* 运营商返回的代码,如:"DB:0103"
*
* 由于不同运营商信息不同,此字段仅供参考;
*/
@JsonProperty("error_msg")
private String errorMsg;
/**
* 运营商反馈代码的中文解释
*
* 默认不推送此字段,如需推送,请联系客服
*/
@JsonProperty("error_detail")
private String errorDetail;
/**
* 短信编号
*/
private Long sid;
/**
* 用户自定义 id
*
* 这里我们传递的是 SysSmsLogDO 的日志编号
*/
private Long uid;
/**
* 用户接收时间
*/
@JsonProperty("user_receive_time")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private Date userReceiveTime;
}
}

View File

@@ -0,0 +1,45 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
import cn.iocoder.dashboard.common.exception.ErrorCode;
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
import static com.yunpian.sdk.constant.Code.*;
/**
* 云片的 SmsCodeMapping 实现类
*
* 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档
*
* @author 芋道源码
*/
public class YunpianSmsCodeMapping implements SmsCodeMapping {
@Override
public ErrorCode apply(String apiCode) {
int code = Integer.parseInt(apiCode);
switch (code) {
case OK: return SUCCESS;
case ARGUMENT_MISSING: return SMS_API_PARAM_ERROR;
case BAD_ARGUMENT_FORMAT: return SMS_TEMPLATE_PARAM_ERROR;
case TPL_NOT_FOUND:
case TPL_NOT_VALID: return SMS_TEMPLATE_INVALID;
case MONEY_NOT_ENOUGH: return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
case BLACK_WORD: return SMS_SEND_CONTENT_INVALID;
case DUP_IN_SHORT_TIME:
case TOO_MANY_TIME_IN_5:
case DAY_LIMIT_PER_MOBILE:
case HOUR_LIMIT_PER_MOBILE: return SMS_SEND_BUSINESS_LIMIT_CONTROL;
case BLACK_PHONE_FILTER: return SMS_MOBILE_BLACK;
case SIGN_NOT_MATCH:
case BAD_SIGN_FORMAT:
case SIGN_NOT_VALID: return SMS_SIGN_INVALID;
case BAD_API_KEY: return SMS_ACCOUNT_INVALID;
case API_NOT_ALLOWED: return SMS_PERMISSION_DENY;
case IP_NOT_ALLOWED: return SMS_IP_DENY;
}
return SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.dashboard.framework.sms.core.enums;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信渠道枚举
*
* @author zzf
* @date 2021/1/25 10:56
*/
@Getter
@AllArgsConstructor
public enum SmsChannelEnum {
DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"),
YUN_PIAN("YUN_PIAN", "云片"),
ALIYUN("ALIYUN", "阿里云"),
// TENCENT("TENCENT", "腾讯云"),
// HUA_WEI("HUA_WEI", "华为云"),
;
/**
* 编码
*/
private final String code;
/**
* 名字
*/
private final String name;
public static SmsChannelEnum getByCode(String code) {
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
}
}

View File

@@ -0,0 +1,47 @@
package cn.iocoder.dashboard.framework.sms.core.enums;
import cn.iocoder.dashboard.common.exception.ErrorCode;
/**
* 短信框架的错误码枚举
*
* 短信框架,使用 2-001-000-000 段
*
* @author 芋道源码
*/
public interface SmsFrameworkErrorCodeConstants {
ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析");
// ========== 权限 / 限流等相关 2001000100 ==========
ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限");
// 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信
ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信");
// 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟5 条 / 小时,累计 10 条 / 天。
ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流");
// 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。
ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流");
ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词");
// ========== 模板相关 2001000200 ==========
ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在
ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确");
// ========== 签名相关 2001000300 ==========
ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用");
// ========== 账户相关 2001000400 ==========
ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足");
ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在");
// ========== 其它相关 2001000900 开头 ==========
ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失");
ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确");
ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中");
ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常");
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.dashboard.framework.sms.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信模板的审核状态枚举
*
* @author 芋道源码
*/
@AllArgsConstructor
@Getter
public enum SmsTemplateAuditStatusEnum {
CHECKING(1),
SUCCESS(2),
FAIL(3);
private final Integer status;
}

View File

@@ -0,0 +1,52 @@
package cn.iocoder.dashboard.framework.sms.core.property;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 短信渠道配置类
*
* @author zzf
* @date 2021/1/25 17:01
*/
@Data
@Validated
public class SmsChannelProperties {
/**
* 渠道编号
*/
@NotNull(message = "短信渠道 ID 不能为空")
private Long id;
/**
* 短信签名
*/
@NotEmpty(message = "短信签名不能为空")
private String signature;
/**
* 渠道编码
*
* 枚举 {@link SmsChannelEnum}
*/
@NotEmpty(message = "渠道编码不能为空")
private String code;
/**
* 短信 API 的账号
*/
@NotEmpty(message = "短信 API 的账号不能为空")
private String apiKey;
/**
* 短信 API 的秘钥
*/
@NotEmpty(message = "短信 API 的秘钥不能为空")
private String apiSecret;
/**
* 短信发送回调 URL
*/
private String callbackUrl;
}

View File

@@ -10,15 +10,17 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.*;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import springfox.documentation.service.ApiKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

View File

@@ -2,7 +2,9 @@ package cn.iocoder.dashboard.framework.web.config;
import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum;
import cn.iocoder.dashboard.framework.web.core.filter.CacheRequestBodyFilter;
import cn.iocoder.dashboard.framework.web.core.filter.DemoFilter;
import cn.iocoder.dashboard.framework.web.core.filter.XssFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@@ -27,9 +29,10 @@ public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
// 设置 API 前缀,仅仅匹配 controller 包下的
configurer.addPathPrefix(webProperties.getApiPrefix(), clazz ->
clazz.isAnnotationPresent(RestController.class)
&& clazz.getPackage().getName().startsWith(webProperties.getControllerPackage()));
&& clazz.getPackage().getName().startsWith(webProperties.getControllerPackage())); // 仅仅匹配 controller 包
}
// ========== Filter 相关 ==========
@@ -67,6 +70,15 @@ public class WebConfiguration implements WebMvcConfigurer {
return createFilterBean(new XssFilter(properties, pathMatcher), FilterOrderEnum.XSS_FILTER);
}
/**
* 创建 DemoFilter Bean演示模式
*/
@Bean
@ConditionalOnProperty(value = "yudao.demo", havingValue = "true")
public FilterRegistrationBean<DemoFilter> demoFilter() {
return createFilterBean(new DemoFilter(), FilterOrderEnum.DEMO_FILTER);
}
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
bean.setOrder(order);

View File

@@ -9,6 +9,7 @@ public interface FilterOrderEnum {
int CORS_FILTER = Integer.MIN_VALUE;
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
// OrderedRequestContextFilter 默认为 -105用于国际化上下文等等
@@ -19,4 +20,6 @@ public interface FilterOrderEnum {
// Spring Security Filter 默认为 -100可见 SecurityProperties 配置属性类
int DEMO_FILTER = Integer.MAX_VALUE;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.dashboard.framework.web.core.filter;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.dashboard.util.servlet.ServletUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY;
/**
* 演示 Filter禁止用户发起写操作避免影响测试数据
*
* @author 芋道源码
*/
public class DemoFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String method = request.getMethod();
return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率
|| SecurityFrameworkUtils.getLoginUser() == null; // 非登陆用户时,不进行过滤
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
// 直接返回 DEMO_DENY 的结果。即,请求不继续
ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY));
}
}

View File

@@ -3,7 +3,6 @@ package cn.iocoder.dashboard.framework.web.core.handler;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.dashboard.common.exception.GlobalException;
import cn.iocoder.dashboard.common.exception.ServiceException;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiErrorLogFrameworkService;
@@ -96,9 +95,6 @@ public class GlobalExceptionHandler {
if (ex instanceof AccessDeniedException) {
return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
}
if (ex instanceof GlobalException) {
return globalExceptionHandler(request, (GlobalException) ex);
}
return defaultExceptionHandler(request, ex);
}
@@ -222,25 +218,6 @@ public class GlobalExceptionHandler {
return CommonResult.error(ex.getCode(), ex.getMessage());
}
/**
* 处理全局异常 ServiceException
*
* 例如说Dubbo 请求超时,调用的 Dubbo 服务系统异常
*/
@ExceptionHandler(value = GlobalException.class)
public CommonResult<?> globalExceptionHandler(HttpServletRequest req, GlobalException ex) {
// 系统异常时,才打印异常日志
if (INTERNAL_SERVER_ERROR.getCode().equals(ex.getCode())) {
// 插入异常日志
this.createExceptionLog(req, ex);
// 普通全局异常,打印 info 日志即可
} else {
log.info("[globalExceptionHandler]", ex);
}
// 返回 ERROR CommonResult
return CommonResult.error(ex);
}
/**
* 处理系统异常,兜底处理所有的一切
*/
@@ -250,7 +227,7 @@ public class GlobalExceptionHandler {
// 插入异常日志
this.createExceptionLog(req, ex);
// 返回 ERROR CommonResult
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage());
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}
private void createExceptionLog(HttpServletRequest req, Throwable e) {
@@ -269,7 +246,7 @@ public class GlobalExceptionHandler {
private void initExceptionLog(ApiErrorLogCreateDTO errorLog, HttpServletRequest request, Throwable e) {
// 处理用户信息
errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
errorLog.setUserType(WebFrameworkUtils.getUesrType(request));
errorLog.setUserType(WebFrameworkUtils.getUserType(request));
// 设置异常字段
errorLog.setExceptionName(e.getClass().getName());
errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));

View File

@@ -31,7 +31,7 @@ public class WebFrameworkUtils {
return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID);
}
public static Integer getUesrType(HttpServletRequest request) {
public static Integer getUserType(HttpServletRequest request) {
return UserTypeEnum.ADMIN.getValue(); // TODO 芋艿:等后续优化
}

View File

@@ -1,11 +1,9 @@
package cn.iocoder.dashboard.modules.infra.controller.config;
import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.modules.infra.controller.config.vo.*;
import cn.iocoder.dashboard.modules.infra.convert.config.InfConfigConvert;
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
@@ -13,95 +11,95 @@ import cn.iocoder.dashboard.modules.infra.service.config.InfConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_SENSITIVE;
@Api(tags = "参数配置")
@RestController
@RequestMapping("/infra/config")
@Validated
public class InfConfigController {
@Resource
private InfConfigService configService;
@ApiOperation("获取参数配置分页")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('infra:config:list')")
public CommonResult<PageResult<InfConfigRespVO>> getConfigPage(@Validated InfConfigPageReqVO reqVO) {
PageResult<InfConfigDO> page = configService.getConfigPage(reqVO);
return success(InfConfigConvert.INSTANCE.convertPage(page));
@PostMapping("/create")
@ApiOperation("创建参数配置")
@PreAuthorize("@ss.hasPermission('infra:config:create')")
public CommonResult<Long> createConfig(@Valid @RequestBody InfConfigCreateReqVO reqVO) {
return success(configService.createConfig(reqVO));
}
@ApiOperation("导出参数配置")
@GetMapping("/export")
// @Log(title = "参数管理", businessType = BusinessType.EXPORT)
// @PreAuthorize("@ss.hasPermi('infra:config:export')")
public void exportSysConfig(HttpServletResponse response, @Validated InfConfigExportReqVO reqVO) throws IOException {
List<InfConfigDO> list = configService.getConfigList(reqVO);
// 拼接数据
List<InfConfigExcelVO> excelDataList = InfConfigConvert.INSTANCE.convertList(list);
// 输出
ExcelUtils.write(response, "参数配置.xls", "配置列表",
InfConfigExcelVO.class, excelDataList);
@PutMapping("/update")
@ApiOperation("修改参数配置")
@PreAuthorize("@ss.hasPermission('infra:config:update')")
public CommonResult<Boolean> updateConfig(@Valid @RequestBody InfConfigUpdateReqVO reqVO) {
configService.updateConfig(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除参数配置")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('infra:config:delete')")
public CommonResult<Boolean> deleteConfig(@RequestParam("id") Long id) {
configService.deleteConfig(id);
return success(true);
}
@GetMapping(value = "/get")
@ApiOperation("获得参数配置")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@GetMapping(value = "/get")
// @PreAuthorize("@ss.hasPermi('infra:config:query')")
@PreAuthorize("@ss.hasPermission('infra:config:query')")
public CommonResult<InfConfigRespVO> getConfig(@RequestParam("id") Long id) {
return success(InfConfigConvert.INSTANCE.convert(configService.getConfig(id)));
}
@GetMapping(value = "/get-value-by-key")
@ApiOperation(value = "根据参数键名查询参数值", notes = "敏感配置,不允许返回给前端")
@ApiImplicitParam(name = "key", value = "参数键", required = true, example = "yunai.biz.username", dataTypeClass = String.class)
@GetMapping(value = "/get-value-by-key")
public CommonResult<String> getConfigKey(@RequestParam("key") String key) {
InfConfigDO config = configService.getConfigByKey(key);
if (config == null) {
return null;
}
if (config.getSensitive()) {
throw ServiceExceptionUtil.exception(CONFIG_GET_VALUE_ERROR_IF_SENSITIVE);
throw exception(CONFIG_GET_VALUE_ERROR_IF_SENSITIVE);
}
return success(config.getValue());
}
@ApiOperation("新增参数配置")
@PostMapping("/create")
// @PreAuthorize("@ss.hasPermi('infra:config:add')")
// @Log(title = "参数管理", businessType = BusinessType.INSERT)
@Idempotent(timeout = 60, keyResolver = ExpressionIdempotentKeyResolver.class, keyArg = "#reqVO.key")
public CommonResult<Long> createConfig(@Validated @RequestBody InfConfigCreateReqVO reqVO) {
return success(configService.createConfig(reqVO));
@GetMapping("/page")
@ApiOperation("获取参数配置分页")
@PreAuthorize("@ss.hasPermission('infra:config:query')")
public CommonResult<PageResult<InfConfigRespVO>> getConfigPage(@Valid InfConfigPageReqVO reqVO) {
PageResult<InfConfigDO> page = configService.getConfigPage(reqVO);
return success(InfConfigConvert.INSTANCE.convertPage(page));
}
@ApiOperation("修改参数配置")
@PutMapping("/update")
// @PreAuthorize("@ss.hasPermi('infra:config:edit')")
// @Log(title = "参数管理", businessType = BusinessType.UPDATE)
@Idempotent(timeout = 60)
public CommonResult<Boolean> updateConfig(@Validated @RequestBody InfConfigUpdateReqVO reqVO) {
configService.updateConfig(reqVO);
return success(true);
}
@ApiOperation("删除参数配置")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@DeleteMapping("/delete")
// @PreAuthorize("@ss.hasPermi('infra:config:remove')")
// @Log(title = "参数管理", businessType = BusinessType.DELETE)
public CommonResult<Boolean> deleteConfig(@RequestParam("id") Long id) {
configService.deleteConfig(id);
return success(true);
@GetMapping("/export")
@ApiOperation("导出参数配置")
@PreAuthorize("@ss.hasPermission('infra:config:export')")
@OperateLog(type = EXPORT)
public void exportSysConfig(@Valid InfConfigExportReqVO reqVO,
HttpServletResponse response) throws IOException {
List<InfConfigDO> list = configService.getConfigList(reqVO);
// 拼接数据
List<InfConfigExcelVO> datas = InfConfigConvert.INSTANCE.convertList(list);
// 输出
ExcelUtils.write(response, "参数配置.xls", "数据", InfConfigExcelVO.class, datas);
}
}

View File

@@ -1,6 +1,8 @@
package cn.iocoder.dashboard.modules.infra.controller.doc;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.dashboard.util.servlet.ServletUtils;
import cn.smallbun.screw.core.Configuration;
import cn.smallbun.screw.core.engine.EngineConfig;
import cn.smallbun.screw.core.engine.EngineFileType;
@@ -10,18 +12,18 @@ import cn.smallbun.screw.core.process.ProcessConfig;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
@Api(tags = "数据库文档")
@@ -34,36 +36,79 @@ public class InfDbDocController {
private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator
+ "db-doc";
private static final EngineFileType FILE_OUTPUT_TYPE = EngineFileType.HTML; // 可以设置 Word 或者 Markdown 格式
private static final String DOC_FILE_NAME = "数据库文档";
private static final String DOC_VERSION = "1.0.0";
private static final String DOC_DESCRIPTION = "文档描述";
@Resource
private DataSource dataSource;
@GetMapping("/export-html")
public synchronized void exportHtml(HttpServletResponse response) throws FileNotFoundException {
@ApiOperation("导出 html 格式的数据文档")
@ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
public void exportHtml(@RequestParam(defaultValue = "true") Boolean deleteFile,
HttpServletResponse response) throws IOException {
doExportFile(EngineFileType.HTML, deleteFile, response);
}
@GetMapping("/export-word")
@ApiOperation("导出 word 格式的数据文档")
@ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
public void exportWord(@RequestParam(defaultValue = "true") Boolean deleteFile,
HttpServletResponse response) throws IOException {
doExportFile(EngineFileType.WORD, deleteFile, response);
}
@GetMapping("/export-markdown")
@ApiOperation("导出 markdown 格式的数据文档")
@ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
public void exportMarkdown(@RequestParam(defaultValue = "true") Boolean deleteFile,
HttpServletResponse response) throws IOException {
doExportFile(EngineFileType.MD, deleteFile, response);
}
private void doExportFile(EngineFileType fileOutputType, Boolean deleteFile,
HttpServletResponse response) throws IOException {
String docFileName = DOC_FILE_NAME + "_" + IdUtil.fastSimpleUUID();
String filePath = doExportFile(fileOutputType, docFileName);
String downloadFileName = DOC_FILE_NAME + fileOutputType.getFileSuffix(); //下载后的文件名
try {
// 读取,返回
ServletUtils.writeAttachment(response, downloadFileName, FileUtil.readBytes(filePath));
} finally {
handleDeleteFile(deleteFile, filePath);
}
}
/**
* 输出文件,返回文件路径
*
* @param fileOutputType 文件类型
* @param fileName 文件名, 无需 ".docx" 等文件后缀
* @return 生成的文件所在路径
*/
private String doExportFile(EngineFileType fileOutputType, String fileName) {
try (HikariDataSource dataSource = buildDataSource()) {
// 创建 screw 的配置
Configuration config = Configuration.builder()
.version(DOC_VERSION) // 版本
.description(DOC_DESCRIPTION) // 描述
.dataSource(dataSource) // 数据源
.engineConfig(buildEngineConfig()) // 引擎配置
.engineConfig(buildEngineConfig(fileOutputType, fileName)) // 引擎配置
.produceConfig(buildProcessConfig()) // 处理配置
.build();
// 执行 screw生成数据库文档
new DocumentationExecute(config).execute();
// 读取,返回
ServletUtil.write(response,
new FileInputStream(FILE_OUTPUT_DIR + File.separator + DOC_FILE_NAME + FILE_OUTPUT_TYPE.getFileSuffix()),
MediaType.TEXT_HTML_VALUE);
return FILE_OUTPUT_DIR + File.separator + fileName + fileOutputType.getFileSuffix();
}
}
private void handleDeleteFile(Boolean deleteFile, String filePath) {
if (!deleteFile) {
return;
}
FileUtil.del(filePath);
}
/**
* 创建数据源
*/
@@ -71,7 +116,6 @@ public class InfDbDocController {
private HikariDataSource buildDataSource() {
// 创建 HikariConfig 配置类
HikariConfig hikariConfig = new HikariConfig();
// hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
hikariConfig.setJdbcUrl(dataSourceProperties.getUrl());
hikariConfig.setUsername(dataSourceProperties.getUsername());
hikariConfig.setPassword(dataSourceProperties.getPassword());
@@ -83,13 +127,13 @@ public class InfDbDocController {
/**
* 创建 screw 的引擎配置
*/
private static EngineConfig buildEngineConfig() {
private static EngineConfig buildEngineConfig(EngineFileType fileOutputType, String docFileName) {
return EngineConfig.builder()
.fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径
.openOutputDir(false) // 打开目录
.fileType(FILE_OUTPUT_TYPE) // 文件类型
.fileType(fileOutputType) // 文件类型
.produceType(EngineTemplateType.freemarker) // 文件类型
.fileName(DOC_FILE_NAME) // 自定义文件名称
.fileName(docFileName) // 自定义文件名称
.build();
}

View File

@@ -1,9 +1,13 @@
package cn.iocoder.dashboard.modules.system.controller.common;
package cn.iocoder.dashboard.modules.infra.controller.file;
import cn.hutool.core.io.IoUtil;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.modules.system.dal.dataobject.common.SysFileDO;
import cn.iocoder.dashboard.modules.system.service.common.SysFileService;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFileRespVO;
import cn.iocoder.dashboard.modules.infra.convert.file.InfFileConvert;
import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
import cn.iocoder.dashboard.modules.infra.service.file.InfFileService;
import cn.iocoder.dashboard.util.servlet.ServletUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
@@ -11,40 +15,53 @@ import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
@Api(tags = "文件存储")
@RestController
@RequestMapping("/system/file")
@RequestMapping("/infra/file")
@Validated
@Slf4j
public class SysFileController {
public class InfFileController {
@Resource
private SysFileService fileService;
private InfFileService fileService;
@PostMapping("/upload")
@ApiOperation("上传文件")
@ApiImplicitParams({
@ApiImplicitParam(name = "path", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
@ApiImplicitParam(name = "path", value = "文件路径", required = true, example = "yudaoyuanma.png", dataTypeClass = Long.class)
@ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
@ApiImplicitParam(name = "path", value = "文件路径", required = false, example = "yudaoyuanma.png", dataTypeClass = String.class)
})
@PostMapping("/upload")
public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file,
@RequestParam("path") String path) throws IOException {
return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream())));
}
@DeleteMapping("/delete")
@ApiOperation("删除文件")
@ApiImplicitParam(name = "id", value = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:file:delete')")
public CommonResult<Boolean> deleteFile(@RequestParam("id") String id) {
fileService.deleteFile(id);
return success(true);
}
@GetMapping("/get/{path}")
@ApiOperation("下载文件")
@ApiImplicitParam(name = "path", value = "文件附件", required = true, dataTypeClass = MultipartFile.class)
@GetMapping("/get/{path}")
public void getFile(HttpServletResponse response, @PathVariable("path") String path) throws IOException {
SysFileDO file = fileService.getFile(path);
InfFileDO file = fileService.getFile(path);
if (file == null) {
log.warn("[getFile][path({}) 文件不存在]", path);
response.setStatus(HttpStatus.NOT_FOUND.value());
@@ -53,4 +70,12 @@ public class SysFileController {
ServletUtils.writeAttachment(response, path, file.getContent());
}
@GetMapping("/page")
@ApiOperation("获得文件分页")
@PreAuthorize("@ss.hasPermission('infra:file:query')")
public CommonResult<PageResult<InfFileRespVO>> getFilePage(@Valid InfFilePageReqVO pageVO) {
PageResult<InfFileDO> pageResult = fileService.getFilePage(pageVO);
return success(InfFileConvert.INSTANCE.convertPage(pageResult));
}
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.dashboard.modules.infra.controller.file.vo;
import cn.iocoder.dashboard.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("文件分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class InfFilePageReqVO extends PageParam {
@ApiModelProperty(value = "文件路径", example = "yudao", notes = "模糊匹配")
private String id;
@ApiModelProperty(value = "文件类型", example = "jpg", notes = "模糊匹配")
private String type;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.dashboard.modules.infra.controller.file.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@ApiModel(value = "文件 Response VO", description = "不返回 content 字段,太大")
@Data
public class InfFileRespVO {
@ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg")
private String id;
@ApiModelProperty(value = "文件类型", required = true, example = "jpg")
private String type;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@@ -75,7 +75,7 @@ public class InfJobLogController {
List<InfJobLogDO> list = jobLogService.getJobLogList(exportReqVO);
// 导出 Excel
List<InfJobLogExcelVO> datas = InfJobLogConvert.INSTANCE.convertList02(list);
ExcelUtils.write(response, "定时任务.xls", "数据", InfJobLogExcelVO.class, datas);
ExcelUtils.write(response, "任务日志.xls", "数据", InfJobLogExcelVO.class, datas);
}
}

View File

@@ -14,7 +14,7 @@ import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOU
public class InfApiErrorLogExportReqVO {
@ApiModelProperty(value = "用户编号", example = "666")
private Integer userId;
private Long userId;
@ApiModelProperty(value = "用户类型", example = "1")
private Integer userType;

View File

@@ -19,7 +19,7 @@ import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOU
public class InfApiErrorLogPageReqVO extends PageParam {
@ApiModelProperty(value = "用户编号", example = "666")
private Integer userId;
private Long userId;
@ApiModelProperty(value = "用户类型", example = "1")
private Integer userType;

View File

@@ -30,9 +30,9 @@ public class RedisController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/get-monitor-info")
@ApiOperation("获得 Redis 监控信息")
@PreAuthorize("@ss.hasPermission('infra:redis:get-monitor-info')")
@GetMapping("/get-monitor-info")
public CommonResult<InfRedisMonitorRespVO> getRedisMonitorInfo() {
// 获得 Redis 统计信息
Properties info = stringRedisTemplate.execute((RedisCallback<Properties>) RedisServerCommands::info);
@@ -44,9 +44,9 @@ public class RedisController {
return success(RedisConvert.INSTANCE.build(info, dbSize, commandStats));
}
@GetMapping("/get-key-list")
@ApiOperation("获得 Redis Key 列表")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
@GetMapping("/get-key-list")
public CommonResult<List<InfRedisKeyRespVO>> getKeyList() {
List<RedisKeyDefine> keyDefines = RedisKeyRegistry.list();
return success(RedisConvert.INSTANCE.convertList(keyDefines));

View File

@@ -0,0 +1,18 @@
package cn.iocoder.dashboard.modules.infra.convert.file;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFileRespVO;
import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface InfFileConvert {
InfFileConvert INSTANCE = Mappers.getMapper(InfFileConvert.class);
InfFileRespVO convert(InfFileDO bean);
PageResult<InfFileRespVO> convertPage(PageResult<InfFileDO> page);
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.dashboard.modules.infra.dal.dataobject.file;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.io.InputStream;
/**
* 文件表
*
* @author 芋道源码
*/
@Data
@TableName("inf_file")
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfFileDO extends BaseDO {
/**
* 文件路径
*/
@TableId(type = IdType.INPUT)
private String id;
/**
* 文件类型
*
* 通过 {@link cn.hutool.core.io.FileTypeUtil#getType(InputStream)} 获取
*/
@TableField(value = "`type`")
private String type;
/**
* 文件内容
*/
private byte[] content;
}

View File

@@ -37,7 +37,7 @@ public class InfApiAccessLogDO extends BaseDO {
/**
* 用户编号
*/
private Integer userId;
private Long userId;
/**
* 用户类型
*

View File

@@ -30,7 +30,7 @@ public class InfApiErrorLogDO extends BaseDO {
/**
* 用户编号
*/
private Integer userId;
private Long userId;
/**
* 链路追踪编号
*
@@ -148,6 +148,6 @@ public class InfApiErrorLogDO extends BaseDO {
*
* 关联 {@link SysUserDO#getId()}
*/
private Integer processUserId;
private Long processUserId;
}

View File

@@ -14,20 +14,21 @@ import java.util.List;
@Mapper
public interface InfConfigMapper extends BaseMapperX<InfConfigDO> {
default PageResult<InfConfigDO> selectPage(InfConfigPageReqVO reqVO) {
return selectPage(reqVO,
new QueryWrapperX<InfConfigDO>().likeIfPresent("name", reqVO.getName())
.likeIfPresent("`key`", reqVO.getKey())
.eqIfPresent("`type`", reqVO.getType())
.betweenIfPresent("create_time", reqVO.getBeginTime(), reqVO.getEndTime()));
}
default InfConfigDO selectByKey(String key) {
return selectOne(new QueryWrapper<InfConfigDO>().eq("`key`", key));
}
default PageResult<InfConfigDO> selectPage(InfConfigPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<InfConfigDO>()
.likeIfPresent("name", reqVO.getName())
.likeIfPresent("`key`", reqVO.getKey())
.eqIfPresent("`type`", reqVO.getType())
.betweenIfPresent("create_time", reqVO.getBeginTime(), reqVO.getEndTime()));
}
default List<InfConfigDO> selectList(InfConfigExportReqVO reqVO) {
return selectList(new QueryWrapperX<InfConfigDO>().likeIfPresent("name", reqVO.getName())
return selectList(new QueryWrapperX<InfConfigDO>()
.likeIfPresent("name", reqVO.getName())
.likeIfPresent("`key`", reqVO.getKey())
.eqIfPresent("`type`", reqVO.getType())
.betweenIfPresent("create_time", reqVO.getBeginTime(), reqVO.getEndTime()));

View File

@@ -0,0 +1,25 @@
package cn.iocoder.dashboard.modules.infra.dal.mysql.file;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InfFileMapper extends BaseMapperX<InfFileDO> {
default Integer selectCountById(String id) {
return selectCount("id", id);
}
default PageResult<InfFileDO> selectPage(InfFilePageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<InfFileDO>()
.likeIfPresent("id", reqVO.getId())
.likeIfPresent("type", reqVO.getType())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("create_time"));
}
}

View File

@@ -10,7 +10,7 @@ import cn.iocoder.dashboard.common.exception.ErrorCode;
public interface InfErrorCodeConstants {
// ========== 参数配置 1001000000 ==========
ErrorCode CONFIG_NOT_FOUND = new ErrorCode(1001000001, "参数配置不存在");
ErrorCode CONFIG_NOT_EXISTS = new ErrorCode(1001000001, "参数配置不存在");
ErrorCode CONFIG_KEY_DUPLICATE = new ErrorCode(1001000002, "参数配置 key 重复");
ErrorCode CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE = new ErrorCode(1001000003, "不能删除类型为系统内置的参数配置");
ErrorCode CONFIG_GET_VALUE_ERROR_IF_SENSITIVE = new ErrorCode(1001000004, "不允许获取敏感配置到前端");
@@ -27,4 +27,7 @@ public interface InfErrorCodeConstants {
ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1001002000, "API 错误日志不存在");
ErrorCode API_ERROR_LOG_PROCESSED = new ErrorCode(1001002001, "API 错误日志已处理");
// ========== 文件 1001003000 ==========
ErrorCode FILE_NOT_EXISTS = new ErrorCode(1001003000, "文件不存在");
}

View File

@@ -7,28 +7,37 @@ import cn.iocoder.dashboard.modules.infra.controller.config.vo.InfConfigPageReqV
import cn.iocoder.dashboard.modules.infra.controller.config.vo.InfConfigUpdateReqVO;
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
import javax.validation.Valid;
import java.util.List;
/**
* 参数配置 Service 接口
*
* @author 芋道源码
*/
public interface InfConfigService {
/**
* 获得参数配置分页列表
* 创建参数配置
*
* @param reqVO 分页条件
* @return 分页列表
* @param reqVO 创建信息
* @return 配置编号
*/
PageResult<InfConfigDO> getConfigPage(InfConfigPageReqVO reqVO);
Long createConfig(@Valid InfConfigCreateReqVO reqVO);
/**
* 获得参数配置列表
* 更新参数配置
*
* @param reqVO 列表
* @return 列表
* @param reqVO 更新信息
*/
List<InfConfigDO> getConfigList(InfConfigExportReqVO reqVO);
void updateConfig(@Valid InfConfigUpdateReqVO reqVO);
/**
* 删除参数配置
*
* @param id 配置编号
*/
void deleteConfig(Long id);
/**
* 获得参数配置
@@ -47,25 +56,20 @@ public interface InfConfigService {
InfConfigDO getConfigByKey(String key);
/**
* 创建参数配置
* 获得参数配置分页列表
*
* @param reqVO 创建信息
* @return 配置编号
* @param reqVO 分页条件
* @return 分页列表
*/
Long createConfig(InfConfigCreateReqVO reqVO);
PageResult<InfConfigDO> getConfigPage(@Valid InfConfigPageReqVO reqVO);
/**
* 更新参数配置
* 获得参数配置列表
*
* @param reqVO 更新信息
* @param reqVO 列表
* @return 列表
*/
void updateConfig(InfConfigUpdateReqVO reqVO);
List<InfConfigDO> getConfigList(@Valid InfConfigExportReqVO reqVO);
/**
* 删除参数配置
*
* @param id 配置编号
*/
void deleteConfig(Long id);
}

View File

@@ -12,8 +12,10 @@ import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
import cn.iocoder.dashboard.modules.infra.enums.config.InfConfigTypeEnum;
import cn.iocoder.dashboard.modules.infra.mq.producer.config.InfConfigProducer;
import cn.iocoder.dashboard.modules.infra.service.config.InfConfigService;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
@@ -26,6 +28,7 @@ import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.*;
*/
@Service
@Slf4j
@Validated
public class InfConfigServiceImpl implements InfConfigService {
@Resource
@@ -34,26 +37,6 @@ public class InfConfigServiceImpl implements InfConfigService {
@Resource
private InfConfigProducer configProducer;
@Override
public PageResult<InfConfigDO> getConfigPage(InfConfigPageReqVO reqVO) {
return configMapper.selectPage(reqVO);
}
@Override
public List<InfConfigDO> getConfigList(InfConfigExportReqVO reqVO) {
return configMapper.selectList(reqVO);
}
@Override
public InfConfigDO getConfig(Long id) {
return configMapper.selectById(id);
}
@Override
public InfConfigDO getConfigByKey(String key) {
return configMapper.selectByKey(key);
}
@Override
public Long createConfig(InfConfigCreateReqVO reqVO) {
// 校验正确性
@@ -92,6 +75,26 @@ public class InfConfigServiceImpl implements InfConfigService {
configProducer.sendConfigRefreshMessage();
}
@Override
public InfConfigDO getConfig(Long id) {
return configMapper.selectById(id);
}
@Override
public InfConfigDO getConfigByKey(String key) {
return configMapper.selectByKey(key);
}
@Override
public PageResult<InfConfigDO> getConfigPage(InfConfigPageReqVO reqVO) {
return configMapper.selectPage(reqVO);
}
@Override
public List<InfConfigDO> getConfigList(InfConfigExportReqVO reqVO) {
return configMapper.selectList(reqVO);
}
private void checkCreateOrUpdate(Long id, String key) {
// 校验自己存在
checkConfigExists(id);
@@ -99,18 +102,20 @@ public class InfConfigServiceImpl implements InfConfigService {
checkConfigKeyUnique(id, key);
}
private InfConfigDO checkConfigExists(Long id) {
@VisibleForTesting
public InfConfigDO checkConfigExists(Long id) {
if (id == null) {
return null;
}
InfConfigDO config = configMapper.selectById(id);
if (config == null) {
throw ServiceExceptionUtil.exception(CONFIG_NOT_FOUND);
throw ServiceExceptionUtil.exception(CONFIG_NOT_EXISTS);
}
return config;
}
private void checkConfigKeyUnique(Long id, String key) {
@VisibleForTesting
public void checkConfigKeyUnique(Long id, String key) {
InfConfigDO config = configMapper.selectByKey(key);
if (config == null) {
return;

View File

@@ -0,0 +1,46 @@
package cn.iocoder.dashboard.modules.infra.service.file;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
/**
* 文件 Service 接口
*
* @author 芋道源码
*/
public interface InfFileService {
/**
* 保存文件,并返回文件的访问路径
*
* @param path 文件路径
* @param content 文件内容
* @return 文件路径
*/
String createFile(String path, byte[] content);
/**
* 删除文件
*
* @param id 编号
*/
void deleteFile(String id);
/**
* 获得文件
*
* @param path 文件路径
* @return 文件
*/
InfFileDO getFile(String path);
/**
* 获得文件分页
*
* @param pageReqVO 分页查询
* @return 文件分页
*/
PageResult<InfFileDO> getFilePage(InfFilePageReqVO pageReqVO);
}

View File

@@ -0,0 +1,72 @@
package cn.iocoder.dashboard.modules.infra.service.file.impl;
import cn.hutool.core.io.FileTypeUtil;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.file.config.FileProperties;
import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
import cn.iocoder.dashboard.modules.infra.dal.mysql.file.InfFileMapper;
import cn.iocoder.dashboard.modules.infra.service.file.InfFileService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.FILE_NOT_EXISTS;
import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.FILE_PATH_EXISTS;
/**
* 文件 Service 实现类
*
* @author 芋道源码
*/
@Service
public class InfFileServiceImpl implements InfFileService {
@Resource
private InfFileMapper fileMapper;
@Resource
private FileProperties fileProperties;
@Override
public String createFile(String path, byte[] content) {
if (fileMapper.selectCountById(path) > 0) {
throw exception(FILE_PATH_EXISTS);
}
// 保存到数据库
InfFileDO file = new InfFileDO();
file.setId(path);
file.setType(FileTypeUtil.getType(new ByteArrayInputStream(content)));
file.setContent(content);
fileMapper.insert(file);
// 拼接路径返回
return fileProperties.getBasePath() + path;
}
@Override
public void deleteFile(String id) {
// 校验存在
this.validateFileExists(id);
// 更新
fileMapper.deleteById(id);
}
private void validateFileExists(String id) {
if (fileMapper.selectById(id) == null) {
throw exception(FILE_NOT_EXISTS);
}
}
@Override
public InfFileDO getFile(String path) {
return fileMapper.selectById(path);
}
@Override
public PageResult<InfFileDO> getFilePage(InfFilePageReqVO pageReqVO) {
return fileMapper.selectPage(pageReqVO);
}
}

View File

@@ -41,7 +41,7 @@ public class InfJobServiceImpl implements InfJobService {
private SchedulerManager schedulerManager;
@Override
@Transactional
@Transactional(rollbackFor = Exception.class)
public Long createJob(InfJobCreateReqVO createReqVO) throws SchedulerException {
validateCronExpression(createReqVO.getCronExpression());
// 校验唯一性
@@ -66,7 +66,7 @@ public class InfJobServiceImpl implements InfJobService {
}
@Override
@Transactional
@Transactional(rollbackFor = Exception.class)
public void updateJob(InfJobUpdateReqVO updateReqVO) throws SchedulerException {
validateCronExpression(updateReqVO.getCronExpression());
// 校验存在
@@ -86,7 +86,7 @@ public class InfJobServiceImpl implements InfJobService {
}
@Override
@Transactional
@Transactional(rollbackFor = Exception.class)
public void updateJobStatus(Long id, Integer status) throws SchedulerException {
// 校验 status
if (!containsAny(status, InfJobStatusEnum.NORMAL.getStatus(), InfJobStatusEnum.STOP.getStatus())) {
@@ -120,7 +120,7 @@ public class InfJobServiceImpl implements InfJobService {
}
@Override
@Transactional
@Transactional(rollbackFor = Exception.class)
public void deleteJob(Long id) throws SchedulerException {
// 校验存在
InfJobDO job = this.validateJobExists(id);

View File

@@ -9,12 +9,13 @@ import cn.iocoder.dashboard.modules.infra.dal.dataobject.logger.InfApiAccessLogD
import cn.iocoder.dashboard.modules.infra.dal.mysql.logger.InfApiAccessLogMapper;
import cn.iocoder.dashboard.modules.infra.service.logger.InfApiAccessLogService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import java.util.concurrent.Future;
/**
* API 访问日志 Service 实现类
@@ -30,10 +31,11 @@ public class InfApiAccessLogServiceImpl implements InfApiAccessLogService {
@Override
@Async
public void createApiAccessLogAsync(ApiAccessLogCreateDTO createDTO) {
public Future<Boolean> createApiAccessLogAsync(ApiAccessLogCreateDTO createDTO) {
// 插入
InfApiAccessLogDO apiAccessLog = InfApiAccessLogConvert.INSTANCE.convert(createDTO);
apiAccessLogMapper.insert(apiAccessLog);
int insert = apiAccessLogMapper.insert(apiAccessLog);
return new AsyncResult<>(insert == 1);
}
@Override

View File

@@ -10,12 +10,14 @@ import cn.iocoder.dashboard.modules.infra.dal.mysql.logger.InfApiErrorLogMapper;
import cn.iocoder.dashboard.modules.infra.enums.logger.InfApiErrorLogProcessStatusEnum;
import cn.iocoder.dashboard.modules.infra.service.logger.InfApiErrorLogService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Future;
import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.API_ERROR_LOG_NOT_FOUND;
@@ -35,10 +37,11 @@ public class InfApiErrorLogServiceImpl implements InfApiErrorLogService {
@Override
@Async
public void createApiErrorLogAsync(ApiErrorLogCreateDTO createDTO) {
public Future<Boolean> createApiErrorLogAsync(ApiErrorLogCreateDTO createDTO) {
InfApiErrorLogDO apiErrorLog = InfApiErrorLogConvert.INSTANCE.convert(createDTO);
apiErrorLog.setProcessStatus(InfApiErrorLogProcessStatusEnum.INIT.getStatus());
apiErrorLogMapper.insert(apiErrorLog);
int insert = apiErrorLogMapper.insert(apiErrorLog);
return new AsyncResult<>(insert == 1);
}
@Override
@@ -62,7 +65,7 @@ public class InfApiErrorLogServiceImpl implements InfApiErrorLogService {
}
// 标记处理
apiErrorLogMapper.updateById(InfApiErrorLogDO.builder().id(id).processStatus(processStatus)
.processUserId(processStatus).processTime(new Date()).build());
.processUserId(processUserId).processTime(new Date()).build());
}
}

View File

@@ -19,6 +19,7 @@ import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
import cn.iocoder.dashboard.util.collection.SetUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@@ -34,6 +35,7 @@ import static cn.iocoder.dashboard.util.servlet.ServletUtils.getUserAgent;
@Api(tags = "认证")
@RestController
@RequestMapping("/")
@Validated
public class SysAuthController {
@Resource
@@ -45,8 +47,8 @@ public class SysAuthController {
@Resource
private SysPermissionService permissionService;
@ApiOperation("使用账号密码登录")
@PostMapping("/login")
@ApiOperation("使用账号密码登录")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<SysAuthLoginRespVO> login(@RequestBody @Valid SysAuthLoginReqVO reqVO) {
String token = authService.login(reqVO, getClientIP(), getUserAgent());
@@ -54,8 +56,8 @@ public class SysAuthController {
return success(SysAuthLoginRespVO.builder().token(token).build());
}
@ApiOperation("获取登陆用户的权限信息")
@GetMapping("/get-permission-info")
@ApiOperation("获取登陆用户的权限信息")
public CommonResult<SysAuthPermissionInfoRespVO> getPermissionInfo() {
// 获得用户信息
SysUserDO user = userService.getUser(getLoginUserId());
@@ -63,9 +65,9 @@ public class SysAuthController {
return null;
}
// 获得角色列表
List<SysRoleDO> roleList = roleService.listRolesFromCache(getLoginUserRoleIds());
List<SysRoleDO> roleList = roleService.getRolesFromCache(getLoginUserRoleIds());
// 获得菜单列表
List<SysMenuDO> menuList = permissionService.listRoleMenusFromCache(
List<SysMenuDO> menuList = permissionService.getRoleMenusFromCache(
getLoginUserRoleIds(), // 注意,基于登陆的角色,因为后续的权限判断也是基于它
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()),
SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus()));
@@ -73,11 +75,11 @@ public class SysAuthController {
return success(SysAuthConvert.INSTANCE.convert(user, roleList, menuList));
}
@ApiOperation("获得登陆用户的菜单列表")
@GetMapping("list-menus")
public CommonResult<List<SysAuthMenuRespVO>> listMenus() {
@ApiOperation("获得登陆用户的菜单列表")
public CommonResult<List<SysAuthMenuRespVO>> getMenus() {
// 获得用户拥有的菜单列表
List<SysMenuDO> menuList = permissionService.listRoleMenusFromCache(
List<SysMenuDO> menuList = permissionService.getRoleMenusFromCache(
getLoginUserRoleIds(), // 注意,基于登陆的角色,因为后续的权限判断也是基于它
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型
SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的

View File

@@ -39,9 +39,9 @@ public class SysUserSessionController {
@Resource
private SysDeptService deptService;
@GetMapping("/page")
@ApiOperation("获得 Session 分页列表")
@PreAuthorize("@ss.hasPermission('system:user-session:page')")
@GetMapping("/page")
public CommonResult<PageResult<SysUserSessionPageItemRespVO>> getUserSessionPage(@Validated SysUserSessionPageReqVO reqVO) {
// 获得 Session 分页
PageResult<SysUserSessionDO> pageResult = userSessionService.getUserSessionPage(reqVO);
@@ -66,12 +66,12 @@ public class SysUserSessionController {
return success(new PageResult<>(sessionList, pageResult.getTotal()));
}
@ApiOperation("删除 Session")
@PreAuthorize("@ss.hasPermission('system:user-session:delete')")
@DeleteMapping("/delete")
@ApiOperation("删除 Session")
@ApiImplicitParam(name = "id", value = "Session 编号", required = true, dataTypeClass = String.class,
example = "fe50b9f6-d177-44b1-8da9-72ea34f63db7")
public CommonResult<Boolean> delete(@RequestParam("id") String id) {
@PreAuthorize("@ss.hasPermission('system:user-session:delete')")
public CommonResult<Boolean> deleteUserSession(@RequestParam("id") String id) {
userSessionService.deleteUserSession(id);
return success(true);
}

View File

@@ -21,8 +21,8 @@ public class SysCaptchaController {
@Resource
private SysCaptchaService captchaService;
@ApiOperation("生成图片验证码")
@GetMapping("/get-image")
@ApiOperation("生成图片验证码")
public CommonResult<SysCaptchaImageRespVO> getCaptchaImage() {
return success(captchaService.getCaptchaImage());
}

View File

@@ -9,10 +9,12 @@ import cn.iocoder.dashboard.modules.system.service.dept.SysDeptService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Comparator;
import java.util.List;
@@ -21,65 +23,64 @@ import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
@Api(tags = "部门")
@RestController
@RequestMapping("/system/dept")
@Validated
public class SysDeptController {
@Resource
private SysDeptService deptService;
@ApiOperation("获取部门列表")
// @PreAuthorize("@ss.hasPermi('system:dept:list')")
@PostMapping("create")
@ApiOperation("创建部门")
@PreAuthorize("@ss.hasPermission('system:dept:create')")
public CommonResult<Long> createDept(@Valid @RequestBody SysDeptCreateReqVO reqVO) {
Long deptId = deptService.createDept(reqVO);
return success(deptId);
}
@PutMapping("update")
@ApiOperation("更新部门")
@PreAuthorize("@ss.hasPermission('system:dept:update')")
public CommonResult<Boolean> updateDept(@Valid @RequestBody SysDeptUpdateReqVO reqVO) {
deptService.updateDept(reqVO);
return success(true);
}
@DeleteMapping("delete")
@ApiOperation("删除部门")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('system:dept:delete')")
public CommonResult<Boolean> deleteDept(@RequestParam("id") Long id) {
deptService.deleteDept(id);
return success(true);
}
@GetMapping("/list")
@ApiOperation("获取部门列表")
@PreAuthorize("@ss.hasPermission('system:dept:query')")
public CommonResult<List<SysDeptRespVO>> listDepts(SysDeptListReqVO reqVO) {
List<SysDeptDO> list = deptService.listDepts(reqVO);
List<SysDeptDO> list = deptService.getSimpleDepts(reqVO);
list.sort(Comparator.comparing(SysDeptDO::getSort));
return success(SysDeptConvert.INSTANCE.convertList(list));
}
@ApiOperation(value = "获取部门精简信息列表", notes = "只包含被开启的部门,主要用于前端的下拉选项")
@GetMapping("/list-all-simple")
public CommonResult<List<SysDeptSimpleRespVO>> listSimpleDepts() {
@ApiOperation(value = "获取部门精简信息列表", notes = "只包含被开启的部门,主要用于前端的下拉选项")
public CommonResult<List<SysDeptSimpleRespVO>> getSimpleDepts() {
// 获得部门列表,只要开启状态的
SysDeptListReqVO reqVO = new SysDeptListReqVO();
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
List<SysDeptDO> list = deptService.listDepts(reqVO);
List<SysDeptDO> list = deptService.getSimpleDepts(reqVO);
// 排序后,返回给前端
list.sort(Comparator.comparing(SysDeptDO::getSort));
return success(SysDeptConvert.INSTANCE.convertList02(list));
}
@GetMapping("/get")
@ApiOperation("获得部门信息")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
// @PreAuthorize("@ss.hasPermi('system:dept:query')")
@GetMapping("/get")
@PreAuthorize("@ss.hasPermission('system:dept:query')")
public CommonResult<SysDeptRespVO> getDept(@RequestParam("id") Long id) {
return success(SysDeptConvert.INSTANCE.convert(deptService.getDept(id)));
}
@ApiOperation("新增部门")
@PostMapping("create")
// @PreAuthorize("@ss.hasPermi('system:dept:add')")
// @Log(title = "部门管理", businessType = BusinessType.INSERT)
public CommonResult<Long> createDept(@Validated @RequestBody SysDeptCreateReqVO reqVO) {
Long deptId = deptService.createDept(reqVO);
return success(deptId);
}
@ApiOperation("修改部门")
@PostMapping("update")
// @PreAuthorize("@ss.hasPermi('system:dept:edit')")
// @Log(title = "部门管理", businessType = BusinessType.UPDATE)
public CommonResult<Boolean> updateDept(@Validated @RequestBody SysDeptUpdateReqVO reqVO) {
deptService.updateDept(reqVO);
return success(true);
}
@ApiOperation("删除部门")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PostMapping("delete")
// @PreAuthorize("@ss.hasPermi('system:dept:remove')")
// @Log(title = "部门管理", businessType = BusinessType.DELETE)
public CommonResult<Boolean> deleteDept(@RequestParam("id") Long id) {
deptService.deleteDept(id);
return success(true);
}
}

View File

@@ -4,6 +4,7 @@ import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.modules.system.controller.dept.vo.post.*;
import cn.iocoder.dashboard.modules.system.convert.dept.SysPostConvert;
import cn.iocoder.dashboard.modules.system.dal.dataobject.dept.SysPostDO;
@@ -11,88 +12,88 @@ import cn.iocoder.dashboard.modules.system.service.dept.SysPostService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "岗位")
@RestController
@RequestMapping("/system/post")
@Valid
public class SysPostController {
@Resource
private SysPostService postService;
@ApiOperation(value = "获取岗位精简信息列表", notes = "只包含被开启的岗位,主要用于前端的下拉选项")
@GetMapping("/list-all-simple")
public CommonResult<List<SysPostSimpleRespVO>> listSimplePosts() {
// 获得岗位列表,只要开启状态的
List<SysPostDO> list = postService.listPosts(null, Collections.singleton(CommonStatusEnum.ENABLE.getStatus()));
// 排序后,返回给前端
list.sort(Comparator.comparing(SysPostDO::getSort));
return success(SysPostConvert.INSTANCE.convertList02(list));
}
@ApiOperation("获得岗位分页列表")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('system:post:list')")
public CommonResult<PageResult<SysPostRespVO>> pagePosts(@Validated SysPostPageReqVO reqVO) {
return success(SysPostConvert.INSTANCE.convertPage(postService.pagePosts(reqVO)));
}
@ApiOperation("新增岗位")
@PostMapping("/create")
// @PreAuthorize("@ss.hasPermi('system:post:add')")
// @Log(title = "岗位管理", businessType = BusinessType.INSERT)
public CommonResult<Long> createPost(@Validated @RequestBody SysPostCreateReqVO reqVO) {
@ApiOperation("创建岗位")
@PreAuthorize("@ss.hasPermission('system:post:create')")
public CommonResult<Long> createPost(@Valid @RequestBody SysPostCreateReqVO reqVO) {
Long postId = postService.createPost(reqVO);
return success(postId);
}
@PutMapping("/update")
@ApiOperation("修改岗位")
@PostMapping("/update")
// @PreAuthorize("@ss.hasPermi('system:post:edit')")
// @Log(title = "岗位管理", businessType = BusinessType.UPDATE)
public CommonResult<Boolean> updatePost(@Validated @RequestBody SysPostUpdateReqVO reqVO) {
@PreAuthorize("@ss.hasPermission('system:post:update')")
public CommonResult<Boolean> updatePost(@Valid @RequestBody SysPostUpdateReqVO reqVO) {
postService.updatePost(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除岗位")
@PostMapping("/delete")
// @PreAuthorize("@ss.hasPermi('system:post:remove')")
// @Log(title = "岗位管理", businessType = BusinessType.DELETE)
@PreAuthorize("@ss.hasPermission('system:post:delete')")
public CommonResult<Boolean> deletePost(@RequestParam("id") Long id) {
postService.deletePost(id);
return success(true);
}
@GetMapping(value = "/get")
@ApiOperation("获得岗位信息")
@ApiImplicitParam(name = "id", value = "岗位编号", required = true, example = "1024", dataTypeClass = Long.class)
// @PreAuthorize("@ss.hasPermi('system:post:query')")
@GetMapping(value = "/get")
@PreAuthorize("@ss.hasPermission('system:post:query')")
public CommonResult<SysPostRespVO> getPost(@RequestParam("id") Long id) {
return success(SysPostConvert.INSTANCE.convert(postService.getPost(id)));
}
@GetMapping("/list-all-simple")
@ApiOperation(value = "获取岗位精简信息列表", notes = "只包含被开启的岗位,主要用于前端的下拉选项")
public CommonResult<List<SysPostSimpleRespVO>> getSimplePosts() {
// 获得岗位列表,只要开启状态的
List<SysPostDO> list = postService.getPosts(null, Collections.singleton(CommonStatusEnum.ENABLE.getStatus()));
// 排序后,返回给前端
list.sort(Comparator.comparing(SysPostDO::getSort));
return success(SysPostConvert.INSTANCE.convertList02(list));
}
@GetMapping("/page")
@ApiOperation("获得岗位分页列表")
@PreAuthorize("@ss.hasPermission('system:post:query')")
public CommonResult<PageResult<SysPostRespVO>> getPostPage(@Validated SysPostPageReqVO reqVO) {
return success(SysPostConvert.INSTANCE.convertPage(postService.getPostPage(reqVO)));
}
@GetMapping("/export")
@ApiOperation("岗位管理")
// @Log(title = "岗位管理", businessType = BusinessType.EXPORT)
// @PreAuthorize("@ss.hasPermi('system:post:export')")
@PreAuthorize("@ss.hasPermission('system:post:export')")
@OperateLog(type = EXPORT)
public void export(HttpServletResponse response, @Validated SysPostExportReqVO reqVO) throws IOException {
List<SysPostDO> posts = postService.listPosts(reqVO);
List<SysPostExcelVO> excelDataList = SysPostConvert.INSTANCE.convertList03(posts);
List<SysPostDO> posts = postService.getPosts(reqVO);
List<SysPostExcelVO> data = SysPostConvert.INSTANCE.convertList03(posts);
// 输出
ExcelUtils.write(response, "岗位数据.xls", "岗位列表",
SysPostExcelVO.class, excelDataList);
ExcelUtils.write(response, "岗位数据.xls", "岗位列表", SysPostExcelVO.class, data);
}
}

View File

@@ -25,8 +25,8 @@ public class SysDeptBaseVO {
private Long parentId;
@ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
@NotBlank(message = "显示顺序不能为空")
private String sort;
@NotNull(message = "显示顺序不能为空")
private Integer sort;
@ApiModelProperty(value = "负责人", example = "芋道")
private String leader;

View File

@@ -4,6 +4,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
@@ -24,8 +25,8 @@ public class SysPostBaseVO {
private String code;
@ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
@NotBlank(message = "显示顺序不能为空")
private String sort;
@NotNull(message = "显示顺序不能为空")
private Integer sort;
@ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
private Integer status;

View File

@@ -23,7 +23,7 @@ public class SysPostExcelVO {
private String name;
@ExcelProperty("岗位排序")
private String sort;
private Integer sort;
@ExcelProperty(value = "状态", converter = DictConvert.class)
@DictFormat(SYS_COMMON_STATUS)

View File

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.controller.dict;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.*;
import cn.iocoder.dashboard.modules.system.convert.dict.SysDictDataConvert;
import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
@@ -10,84 +11,85 @@ import cn.iocoder.dashboard.modules.system.service.dict.SysDictDataService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "字典数据")
@RestController
@RequestMapping("/system/dict-data")
@Validated
public class SysDictDataController {
@Resource
private SysDictDataService dictDataService;
@ApiOperation(value = "获得全部字典数据列表", notes = "一般用于管理后台缓存字典数据在本地")
@GetMapping("/list-all-simple")
// 无需添加权限认证,因为前端全局都需要
public CommonResult<List<SysDictDataSimpleVO>> listSimpleDictDatas() {
List<SysDictDataDO> list = dictDataService.listDictDatas();
return success(SysDictDataConvert.INSTANCE.convertList(list));
}
@ApiOperation("/获得字典类型的分页列表")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('system:dict:list')")
public CommonResult<PageResult<SysDictDataRespVO>> pageDictTypes(@Validated SysDictDataPageReqVO reqVO) {
return success(SysDictDataConvert.INSTANCE.convertPage(dictDataService.pageDictDatas(reqVO)));
}
@ApiOperation("/查询字典数据详细")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@GetMapping(value = "/get")
// @PreAuthorize("@ss.hasPermi('system:dict:query')")
public CommonResult<SysDictDataRespVO> getDictData(@RequestParam("id") Long id) {
return success(SysDictDataConvert.INSTANCE.convert(dictDataService.getDictData(id)));
}
@ApiOperation("新增字典数据")
@PostMapping("/create")
// @PreAuthorize("@ss.hasPermi('system:dict:add')")
// @Log(title = "字典数据", businessData = BusinessData.INSERT)
public CommonResult<Long> createDictData(@Validated @RequestBody SysDictDataCreateReqVO reqVO) {
@ApiOperation("新增字典数据")
@PreAuthorize("@ss.hasPermission('system:dict:create')")
public CommonResult<Long> createDictData(@Valid @RequestBody SysDictDataCreateReqVO reqVO) {
Long dictDataId = dictDataService.createDictData(reqVO);
return success(dictDataId);
}
@PutMapping("update")
@ApiOperation("修改字典数据")
@PostMapping("update")
// @PreAuthorize("@ss.hasPermi('system:dict:edit')")
// @Log(title = "字典数据", businessData = BusinessData.UPDATE)
public CommonResult<Boolean> updateDictData(@Validated @RequestBody SysDictDataUpdateReqVO reqVO) {
@PreAuthorize("@ss.hasPermission('system:dict:update')")
public CommonResult<Boolean> updateDictData(@Valid @RequestBody SysDictDataUpdateReqVO reqVO) {
dictDataService.updateDictData(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除字典数据")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PostMapping("/delete")
// @PreAuthorize("@ss.hasPermi('system:dict:remove')")
@PreAuthorize("@ss.hasPermission('system:dict:delete')")
public CommonResult<Boolean> deleteDictData(Long id) {
dictDataService.deleteDictData(id);
return success(true);
}
@ApiOperation("导出字典数据")
@GetMapping("/list-all-simple")
@ApiOperation(value = "获得全部字典数据列表", notes = "一般用于管理后台缓存字典数据在本地")
// 无需添加权限认证,因为前端全局都需要
public CommonResult<List<SysDictDataSimpleRespVO>> getSimpleDictDatas() {
List<SysDictDataDO> list = dictDataService.getDictDatas();
return success(SysDictDataConvert.INSTANCE.convertList(list));
}
@GetMapping("/page")
@ApiOperation("/获得字典类型的分页列表")
@PreAuthorize("@ss.hasPermission('system:dict:query')")
public CommonResult<PageResult<SysDictDataRespVO>> getDictTypePage(@Valid SysDictDataPageReqVO reqVO) {
return success(SysDictDataConvert.INSTANCE.convertPage(dictDataService.getDictDataPage(reqVO)));
}
@GetMapping(value = "/get")
@ApiOperation("/查询字典数据详细")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('system:dict:query')")
public CommonResult<SysDictDataRespVO> getDictData(@RequestParam("id") Long id) {
return success(SysDictDataConvert.INSTANCE.convert(dictDataService.getDictData(id)));
}
@GetMapping("/export")
// @Log(title = "字典类型", businessType = BusinessType.EXPORT)
// @PreAuthorize("@ss.hasPermi('system:dict:export')")
public void export(HttpServletResponse response, @Validated SysDictDataExportReqVO reqVO) throws IOException {
List<SysDictDataDO> list = dictDataService.listDictDatas(reqVO);
List<SysDictDataExcelVO> excelDataList = SysDictDataConvert.INSTANCE.convertList02(list);
@ApiOperation("导出字典数据")
@PreAuthorize("@ss.hasPermission('system:dict:export')")
@OperateLog(type = EXPORT)
public void export(HttpServletResponse response, @Valid SysDictDataExportReqVO reqVO) throws IOException {
List<SysDictDataDO> list = dictDataService.getDictDatas(reqVO);
List<SysDictDataExcelVO> data = SysDictDataConvert.INSTANCE.convertList02(list);
// 输出
ExcelUtils.write(response, "字典数据.xls", "数据列表",
SysDictDataExcelVO.class, excelDataList);
ExcelUtils.write(response, "字典数据.xls", "数据列表", SysDictDataExcelVO.class, data);
}
}

View File

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.controller.dict;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.modules.system.controller.dict.vo.type.*;
import cn.iocoder.dashboard.modules.system.convert.dict.SysDictTypeConvert;
import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictTypeDO;
@@ -10,85 +11,85 @@ import cn.iocoder.dashboard.modules.system.service.dict.SysDictTypeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "字典类型")
@RestController
@RequestMapping("/system/dict-type")
@Validated
public class SysDictTypeController {
@Resource
private SysDictTypeService dictTypeService;
@ApiOperation("/获得字典类型的分页列表")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('system:dict:list')")
public CommonResult<PageResult<SysDictTypeRespVO>> pageDictTypes(@Validated SysDictTypePageReqVO reqVO) {
return success(SysDictTypeConvert.INSTANCE.convertPage(dictTypeService.pageDictTypes(reqVO)));
}
@ApiOperation("/查询字典类型详细")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@GetMapping(value = "/get")
// @PreAuthorize("@ss.hasPermi('system:dict:query')")
public CommonResult<SysDictTypeRespVO> getDictType(@RequestParam("id") Long id) {
return success(SysDictTypeConvert.INSTANCE.convert(dictTypeService.getDictType(id)));
}
@ApiOperation("新增字典类型")
@PostMapping("/create")
// @PreAuthorize("@ss.hasPermi('system:dict:add')")
// @Log(title = "字典类型", businessType = BusinessType.INSERT)
public CommonResult<Long> createDictType(@Validated @RequestBody SysDictTypeCreateReqVO reqVO) {
@ApiOperation("创建字典类型")
@PreAuthorize("@ss.hasPermission('system:dict:create')")
public CommonResult<Long> createDictType(@Valid @RequestBody SysDictTypeCreateReqVO reqVO) {
Long dictTypeId = dictTypeService.createDictType(reqVO);
return success(dictTypeId);
}
@PutMapping("/update")
@ApiOperation("修改字典类型")
@PostMapping("update")
// @PreAuthorize("@ss.hasPermi('system:dict:edit')")
// @Log(title = "字典类型", businessType = BusinessType.UPDATE)
public CommonResult<Boolean> updateDictType(@Validated @RequestBody SysDictTypeUpdateReqVO reqVO) {
@PreAuthorize("@ss.hasPermission('system:dict:update')")
public CommonResult<Boolean> updateDictType(@Valid @RequestBody SysDictTypeUpdateReqVO reqVO) {
dictTypeService.updateDictType(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除字典类型")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PostMapping("/delete")
// @PreAuthorize("@ss.hasPermi('system:dict:remove')")
@PreAuthorize("@ss.hasPermission('system:dict:delete')")
public CommonResult<Boolean> deleteDictType(Long id) {
dictTypeService.deleteDictType(id);
return success(true);
}
@ApiOperation("/获得字典类型的分页列表")
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('system:dict:quey')")
public CommonResult<PageResult<SysDictTypeRespVO>> pageDictTypes(@Valid SysDictTypePageReqVO reqVO) {
return success(SysDictTypeConvert.INSTANCE.convertPage(dictTypeService.getDictTypePage(reqVO)));
}
@ApiOperation("/查询字典类型详细")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@GetMapping(value = "/get")
@PreAuthorize("@ss.hasPermission('system:dict:quey')")
public CommonResult<SysDictTypeRespVO> getDictType(@RequestParam("id") Long id) {
return success(SysDictTypeConvert.INSTANCE.convert(dictTypeService.getDictType(id)));
}
@GetMapping("/list-all-simple")
@ApiOperation(value = "获得全部字典类型列表", notes = "包括开启 + 禁用的字典类型,主要用于前端的下拉选项")
// 无需添加权限认证,因为前端全局都需要
public CommonResult<List<SysDictTypeSimpleRespVO>> listSimpleDictTypes() {
List<SysDictTypeDO> list = dictTypeService.listDictTypes();
List<SysDictTypeDO> list = dictTypeService.getDictTypeList();
return success(SysDictTypeConvert.INSTANCE.convertList(list));
}
@ApiOperation("导出数据类型")
@GetMapping("/export")
// @Log(title = "字典类型", businessType = BusinessType.EXPORT)
// @PreAuthorize("@ss.hasPermi('system:dict:export')")
public void export(HttpServletResponse response, @Validated SysDictTypeExportReqVO reqVO) throws IOException {
List<SysDictTypeDO> list = dictTypeService.listDictTypes(reqVO);
List<SysDictTypeExcelVO> excelTypeList = SysDictTypeConvert.INSTANCE.convertList02(list);
@PreAuthorize("@ss.hasPermission('system:dict:quey')")
@OperateLog(type = EXPORT)
public void export(HttpServletResponse response, @Valid SysDictTypeExportReqVO reqVO) throws IOException {
List<SysDictTypeDO> list = dictTypeService.getDictTypeList(reqVO);
List<SysDictTypeExcelVO> data = SysDictTypeConvert.INSTANCE.convertList02(list);
// 输出
ExcelUtils.write(response, "字典类型.xls", "类型列表",
SysDictTypeExcelVO.class, excelTypeList);
ExcelUtils.write(response, "字典类型.xls", "类型列表", SysDictTypeExcelVO.class, data);
}
}

View File

@@ -15,8 +15,8 @@ import javax.validation.constraints.Size;
public class SysDictDataBaseVO {
@ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
@NotBlank(message = "显示顺序不能为空")
private String sort;
@NotNull(message = "显示顺序不能为空")
private Integer sort;
@ApiModelProperty(value = "字典标签", required = true, example = "芋道")
@NotBlank(message = "字典标签不能为空")

View File

@@ -4,9 +4,9 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ApiModel("数据字典精简 VO")
@ApiModel("数据字典精简 Response VO")
@Data
public class SysDictDataSimpleVO {
public class SysDictDataSimpleRespVO {
@ApiModelProperty(value = "字典类型", required = true, example = "gender")
private String dictType;

View File

@@ -5,7 +5,6 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.Size;
import java.util.Date;
import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -18,18 +17,17 @@ public class SysDictTypeExportReqVO {
private String name;
@ApiModelProperty(value = "字典类型", example = "sys_common_sex", notes = "模糊匹配")
@Size(max = 100, message = "字典类型类型长度不能超过100个字符")
private String type;
@ApiModelProperty(value = "展示状态", example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
private Integer status;
@ApiModelProperty(value = "开始时间", example = "2020-10-24")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date beginTime;
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@ApiModelProperty(value = "结束时间", example = "2020-10-24")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date endTime;
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -27,12 +27,12 @@ public class SysDictTypePageReqVO extends PageParam {
@ApiModelProperty(value = "展示状态", example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
private Integer status;
@ApiModelProperty(value = "开始时间", example = "2020-10-24")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date beginTime;
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@ApiModelProperty(value = "结束时间", example = "2020-10-24")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date endTime;
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.controller.logger;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.loginlog.SysLoginLogExcelVO;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.loginlog.SysLoginLogExportReqVO;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.loginlog.SysLoginLogPageReqVO;
@@ -12,6 +13,7 @@ import cn.iocoder.dashboard.modules.system.dal.dataobject.logger.SysLoginLogDO;
import cn.iocoder.dashboard.modules.system.service.logger.SysLoginLogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -19,36 +21,39 @@ import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "登陆日志")
@RestController
@RequestMapping("/system/login-log")
@Validated
public class SysLoginLogController {
@Resource
private SysLoginLogService loginLogService;
@ApiOperation("获得登陆日志分页列表")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('system:login-log:query')")
public CommonResult<PageResult<SysLoginLogRespVO>> getLoginLogPage(@Validated SysLoginLogPageReqVO reqVO) {
@ApiOperation("获得登陆日志分页列表")
@PreAuthorize("@ss.hasPermission('system:login-log:query')")
public CommonResult<PageResult<SysLoginLogRespVO>> getLoginLogPage(@Valid SysLoginLogPageReqVO reqVO) {
PageResult<SysLoginLogDO> page = loginLogService.getLoginLogPage(reqVO);
return CommonResult.success(SysLoginLogConvert.INSTANCE.convertPage(page));
}
@ApiOperation("导出登陆日志 Excel")
@GetMapping("/export")
// @Log(title = "登录日志", businessType = BusinessType.EXPORT)
// @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
public void exportLoginLog(HttpServletResponse response, @Validated SysLoginLogExportReqVO reqVO) throws IOException {
@ApiOperation("导出登陆日志 Excel")
@PreAuthorize("@ss.hasPermission('system:login-log:export')")
@OperateLog(type = EXPORT)
public void exportLoginLog(HttpServletResponse response, @Valid SysLoginLogExportReqVO reqVO) throws IOException {
List<SysLoginLogDO> list = loginLogService.getLoginLogList(reqVO);
// 拼接数据
List<SysLoginLogExcelVO> excelDataList = SysLoginLogConvert.INSTANCE.convertList(list);
List<SysLoginLogExcelVO> data = SysLoginLogConvert.INSTANCE.convertList(list);
// 输出
ExcelUtils.write(response, "登陆日志.xls", "数据列表",
SysLoginLogExcelVO.class, excelDataList);
ExcelUtils.write(response, "登陆日志.xls", "数据列表", SysLoginLogExcelVO.class, data);
}
}

View File

@@ -4,8 +4,6 @@ import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum;
import cn.iocoder.dashboard.framework.logger.operatelog.core.util.OperateLogUtils;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogExcelVO;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogExportReqVO;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogPageReqVO;
@@ -19,6 +17,7 @@ import cn.iocoder.dashboard.util.collection.CollectionUtils;
import cn.iocoder.dashboard.util.collection.MapUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -26,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -38,33 +38,19 @@ import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.Operat
@Api(tags = "操作日志")
@RestController
@RequestMapping("/system/operate-log")
@Validated
public class SysOperateLogController {
@Resource
private SysOperateLogService operateLogService;
@Resource
private SysUserService userService;
@ApiOperation("示例")
@OperateLog(type = OperateTypeEnum.OTHER)
@GetMapping("/demo")
public CommonResult<Boolean> demo() {
// 这里可以调用业务逻辑
// 补全操作日志的明细
OperateLogUtils.setContent("将编号 1 的数据xxx 字段修改成了 yyyy");
OperateLogUtils.addExt("orderId", 1);
// 响应
return success(true);
}
@ApiOperation("查看操作日志分页列表")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('system:operate-log:query')")
public CommonResult<PageResult<SysOperateLogRespVO>> pageOperateLog(@Validated SysOperateLogPageReqVO reqVO) {
PageResult<SysOperateLogDO> pageResult = operateLogService.pageOperateLog(reqVO);
@ApiOperation("查看操作日志分页列表")
@PreAuthorize("@ss.hasPermission('system:operate-log:query')")
public CommonResult<PageResult<SysOperateLogRespVO>> pageOperateLog(@Valid SysOperateLogPageReqVO reqVO) {
PageResult<SysOperateLogDO> pageResult = operateLogService.getOperateLogPage(reqVO);
// 获得拼接需要的数据
Collection<Long> userIds = CollectionUtils.convertList(pageResult.getList(), SysOperateLogDO::getUserId);
@@ -82,11 +68,10 @@ public class SysOperateLogController {
@ApiOperation("导出操作日志")
@GetMapping("/export")
@PreAuthorize("@ss.hasPermission('system:operate-log:export')")
@OperateLog(type = EXPORT)
// @PreAuthorize("@ss.hasPermi('system:operate-log:export')")
public void exportOperateLog(HttpServletResponse response, @Validated SysOperateLogExportReqVO reqVO)
throws IOException {
List<SysOperateLogDO> list = operateLogService.listOperateLogs(reqVO);
public void exportOperateLog(HttpServletResponse response, @Valid SysOperateLogExportReqVO reqVO) throws IOException {
List<SysOperateLogDO> list = operateLogService.getOperateLogs(reqVO);
// 获得拼接需要的数据
Collection<Long> userIds = CollectionUtils.convertList(list, SysOperateLogDO::getUserId);
@@ -94,8 +79,7 @@ public class SysOperateLogController {
// 拼接数据
List<SysOperateLogExcelVO> excelDataList = SysOperateLogConvert.INSTANCE.convertList(list, userMap);
// 输出
ExcelUtils.write(response, "操作日志.xls", "数据列表",
SysOperateLogExcelVO.class, excelDataList);
ExcelUtils.write(response, "操作日志.xls", "数据列表", SysOperateLogExcelVO.class, excelDataList);
}
}

View File

@@ -11,62 +11,62 @@ import cn.iocoder.dashboard.modules.system.service.notice.SysNoticeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
@Api(tags = "通知公告")
@RestController
@RequestMapping("/system/notice")
@Validated
public class SysNoticeController {
@Resource
private SysNoticeService noticeService;
@ApiOperation("获取通知公告列表")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('system:notice:list')")
public CommonResult<PageResult<SysNoticeRespVO>> pageNotices(@Validated SysNoticePageReqVO reqVO) {
return success(SysNoticeConvert.INSTANCE.convertPage(noticeService.pageNotices(reqVO)));
}
@ApiOperation("获得通知公告")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
// @PreAuthorize("@ss.hasPermi('system:notice:query')")
@GetMapping(value = "/get")
public CommonResult<SysNoticeRespVO> getNotice(@RequestParam("id") Long id) {
return success(SysNoticeConvert.INSTANCE.convert(noticeService.getNotice(id)));
}
@ApiOperation("新增通知公告")
// @PreAuthorize("@ss.hasPermi('system:notice:add')")
// @Log(title = "通知公告", businessType = BusinessType.INSERT)
@PostMapping("/create")
public CommonResult<Long> createNotice(@Validated @RequestBody SysNoticeCreateReqVO reqVO) {
@ApiOperation("创建通知公告")
@PreAuthorize("@ss.hasPermission('system:notice:create')")
public CommonResult<Long> createNotice(@Valid @RequestBody SysNoticeCreateReqVO reqVO) {
Long noticeId = noticeService.createNotice(reqVO);
return success(noticeId);
}
@PutMapping("/update")
@ApiOperation("修改通知公告")
// @PreAuthorize("@ss.hasPermi('system:notice:edit')")
// @Log(title = "通知公告", businessType = BusinessType.UPDATE)
@PostMapping("/update")
public CommonResult<Boolean> updateNotice(@Validated @RequestBody SysNoticeUpdateReqVO reqVO) {
@PreAuthorize("@ss.hasPermission('system:notice:update')")
public CommonResult<Boolean> updateNotice(@Valid @RequestBody SysNoticeUpdateReqVO reqVO) {
noticeService.updateNotice(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除通知公告")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
// @PreAuthorize("@ss.hasPermi('system:notice:remove')")
// @Log(title = "通知公告", businessType = BusinessType.DELETE)
@PostMapping("/delete")
@PreAuthorize("@ss.hasPermission('system:notice:delete')")
public CommonResult<Boolean> deleteNotice(@RequestParam("id") Long id) {
noticeService.deleteNotice(id);
return success(true);
}
@GetMapping("/page")
@ApiOperation("获取通知公告列表")
@PreAuthorize("@ss.hasPermission('system:notice:quey')")
public CommonResult<PageResult<SysNoticeRespVO>> pageNotices(@Validated SysNoticePageReqVO reqVO) {
return success(SysNoticeConvert.INSTANCE.convertPage(noticeService.pageNotices(reqVO)));
}
@GetMapping("/get")
@ApiOperation("获得通知公告")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('system:notice:quey')")
public CommonResult<SysNoticeRespVO> getNotice(@RequestParam("id") Long id) {
return success(SysNoticeConvert.INSTANCE.convert(noticeService.getNotice(id)));
}
}

Some files were not shown because too many files have changed in this diff Show More