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

 Conflicts:
	src/main/resources/application.yaml
This commit is contained in:
YunaiV 2021-04-18 22:12:34 +08:00
commit 54e26d9e8b
317 changed files with 16328 additions and 3956 deletions

View File

@ -35,12 +35,12 @@
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
| | 岗位管理 | 配置系统用户所属担任职务 |
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
| | 通知公告 | 系统通知公告信息发布维护 |
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、云片等主流短信平台 |
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
| | 登录日志 | 系统登录日志记录查询包含登录异常 |
| | 通知公告 | 系统通知公告信息发布维护 |
计划新增功能:
* 短信
* 邮件
* 钉钉、飞书等通知
@ -50,6 +50,7 @@
| --- | --- | --- |
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
| | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
| 🚀 | 文件服务 | 支持本地文件存储,同时支持兼容 Amazon S3 协议的云服务、开源组件 |
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
| | MySQL 监控 | 监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈 |
| | Redis 监控 |监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
@ -64,7 +65,6 @@
计划新增:
* 工作流
* 错误码
* 文件服务
### 研发工具
@ -87,14 +87,15 @@
## 技术栈
**后端**
### 后端
| 框架 | 说明 | 版本 | 学习指南 |
| --- | --- | --- | --- |
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.4.2 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 | |
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.4 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis-Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 | |
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.1.46 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.4.2 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
@ -102,16 +103,16 @@
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.1.7 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.2 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
| [Resilience4j](https://github.com/quartz-scheduler) | 服务保障组件 | 1.7.0 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.0 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
| [Spring Boot Admin](https://github.com/skywalking) | Spring Boot 监控平台 | 8.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 8.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.11.4 | |
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.7.0 | - |
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 3.6.28 | - |
**前端**
### 前端
| 框架 | 说明 | 版本 |
| --- | --- | --- |

4
lombok.config Normal file
View File

@ -0,0 +1,4 @@
config.stopBubbling = true
lombok.tostring.callsuper=true
lombok.equalsandhashcode.callsuper=true
lombok.accessors.chain=true

73
pom.xml
View File

@ -22,15 +22,16 @@
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
<!-- 统一依赖管理 -->
<spring.boot.version>2.4.2</spring.boot.version>
<spring.boot.version>2.4.4</spring.boot.version>
<!-- Web 相关 -->
<knife4j.version>3.0.2</knife4j.version>
<swagger-annotations.version>1.5.22</swagger-annotations.version>
<!-- DB 相关 -->
<mysql-connector-java.version>5.1.46</mysql-connector-java.version>
<druid.version>1.2.4</druid.version>
<mybatis-plus.version>3.4.1</mybatis-plus.version>
<redisson.version>3.14.1</redisson.version>
<mybatis-plus.version>3.4.2</mybatis-plus.version>
<dynamic-datasource.version>3.3.2</dynamic-datasource.version>
<redisson.version>3.15.1</redisson.version>
<!-- Config 配置中心相关 -->
<apollo.version>1.7.0</apollo.version>
<!-- 服务保障相关 -->
@ -42,10 +43,16 @@
<!-- 工具类相关 -->
<lombok.version>1.16.14</lombok.version>
<mapstruct.version>1.4.1.Final</mapstruct.version>
<hutool.version>5.5.6</hutool.version>
<hutool.version>5.6.1</hutool.version>
<easyexcel.verion>2.2.7</easyexcel.verion>
<velocity.version>2.2</velocity.version>
<screw.version>1.0.5</screw.version>
<podam.version>7.2.6.RELEASE</podam.version>
<jedis-mock.version>0.1.16</jedis-mock.version>
<!-- 三方云服务相关 -->
<aliyun-java-sdk-core.version>4.5.18</aliyun-java-sdk-core.version>
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
</properties>
<!-- 依赖声明 -->
@ -133,6 +140,11 @@
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
<version>${dynamic-datasource.version}</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
@ -212,14 +224,14 @@
<dependency>
<groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
<artifactId>jedis-mock</artifactId>
<version>0.1.16</version>
<version>${jedis-mock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
<artifactId>podam</artifactId>
<version>7.2.6.RELEASE</version>
<version>${podam.version}</version>
<scope>test</scope>
</dependency>
@ -235,6 +247,12 @@
<artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
@ -243,27 +261,7 @@
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
@ -285,6 +283,27 @@
<version>${screw.version}</version>
</dependency>
<!-- 三方云服务相关 -->
<!-- SMS SDK begin -->
<dependency>
<groupId>com.yunpian.sdk</groupId>
<artifactId>yunpian-java-sdk</artifactId>
<version>${yunpian-java-sdk.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>${aliyun-java-sdk-core.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>${aliyun-java-sdk-dysmsapi.version}</version>
</dependency>
<!-- SMS SDK end -->
</dependencies>

View File

@ -1,27 +0,0 @@
package com.ruoyi.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 数据权限过滤注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别名
*/
public String userAlias() default "";
}

View File

@ -1,145 +0,0 @@
package com.ruoyi.framework.aspectj;
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.web.service.TokenService;
/**
* 数据过滤处理
*
* @author ruoyi
*/
@Aspect
@Component
public class DataScopeAspect {
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";
/**
* 数据权限过滤关键字
*/
public static final String DATA_SCOPE = "dataScope";
// 配置织入点
@Pointcut("@annotation(com.ruoyi.common.annotation.DataScope)")
public void dataScopePointCut() {
}
@Before("dataScopePointCut()")
public void doBefore(JoinPoint point) throws Throwable {
handleDataScope(point);
}
protected void handleDataScope(final JoinPoint joinPoint) {
// 获得注解
DataScope controllerDataScope = getAnnotationLog(joinPoint);
if (controllerDataScope == null) {
return;
}
// 获取当前的用户
LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNotNull(loginUser)) {
SysUser currentUser = loginUser.getUser();
// 如果是超级管理员则不过滤数据
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) {
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias());
}
}
}
/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param userAlias 别名
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) {
StringBuilder sqlString = new StringBuilder();
for (SysRole role : user.getRoles()) {
String dataScope = role.getDataScope();
if (DATA_SCOPE_ALL.equals(dataScope)) {
sqlString = new StringBuilder();
break;
} else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
} else if (DATA_SCOPE_DEPT.equals(dataScope)) {
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
} else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
} else if (DATA_SCOPE_SELF.equals(dataScope)) {
if (StringUtils.isNotBlank(userAlias)) {
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
} else {
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(" OR 1=0 ");
}
}
}
if (StringUtils.isNotBlank(sqlString.toString())) {
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}
/**
* 是否存在注解如果存在就获取
*/
private DataScope getAnnotationLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(DataScope.class);
}
return null;
}
}

View File

@ -1,28 +0,0 @@
package com.ruoyi.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.ruoyi.common.enums.DataSourceType;
/**
* 自定义多数据源切换注解
* <p>
* 优先级先方法后类如果方法覆盖了类上的数据源类型以方法的为准否则以类上的为准
*
* @author ruoyi
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
/**
* 切换数据源名称
*/
public DataSourceType value() default DataSourceType.MASTER;
}

View File

@ -1,64 +0,0 @@
package com.ruoyi.framework.aspectj;
import java.util.Objects;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder;
/**
* 多数据源处理
*
* @author ruoyi
*/
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
protected Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
+ "|| @within(com.ruoyi.common.annotation.DataSource)")
public void dsPointCut() {
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);
if (StringUtils.isNotNull(dataSource)) {
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
}
try {
return point.proceed();
} finally {
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
/**
* 获取需要切换的数据源
*/
public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (Objects.nonNull(dataSource)) {
return dataSource;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}

View File

@ -1,19 +0,0 @@
package com.ruoyi.common.enums;
/**
* 数据源
*
* @author ruoyi
*/
public enum DataSourceType
{
/**
* 主库
*/
MASTER,
/**
* 从库
*/
SLAVE
}

View File

@ -1,24 +0,0 @@
package com.ruoyi.framework.datasource;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源
*
* @author ruoyi
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}

View File

@ -1,41 +0,0 @@
package com.ruoyi.framework.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 数据源切换处理
*
* @author ruoyi
*/
public class DynamicDataSourceContextHolder {
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* 使用ThreadLocal维护变量ThreadLocal为每个使用该变量的线程提供独立的变量副本
* 所以每一个线程都可以独立地改变自己的副本而不会影响其它线程所对应的副本
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源的变量
*/
public static void setDataSourceType(String dsType) {
log.info("切换到{}数据源", dsType);
CONTEXT_HOLDER.set(dsType);
}
/**
* 获得数据源的变量
*/
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}

View File

@ -1,116 +0,0 @@
package com.ruoyi.framework.config;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.DruidProperties;
import com.ruoyi.framework.datasource.DynamicDataSource;
/**
* druid 配置多数据源
*
* @author ruoyi
*/
@Configuration
public class DruidConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
return new DynamicDataSource(masterDataSource, targetDataSources);
}
/**
* 设置数据源
*
* @param targetDataSources 备选数据源集合
* @param sourceName 数据源名称
* @param beanName bean名称
*/
public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
try {
DataSource dataSource = SpringUtils.getBean(beanName);
targetDataSources.put(sourceName, dataSource);
} catch (Exception e) {
}
}
/**
* 去除监控页面底部的广告
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
@ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
// 获取web监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
final String filePath = "support/http/resources/js/common.js";
// 创建filter进行过滤
Filter filter = new Filter() {
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
// 重置缓冲区响应头不会被重置
response.resetBuffer();
// 获取common.js
String text = Utils.readFromResource(filePath);
// 正则替换banner, 除去底部的广告信息
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
text = text.replaceAll("powered.*?shrek.wang</a>", "");
response.getWriter().write(text);
}
@Override
public void destroy() {
}
};
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}

View File

@ -1,37 +0,0 @@
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false

7
ruoyi-ui/.env.demo1024 Normal file
View File

@ -0,0 +1,7 @@
NODE_ENV = production
# 测试环境配置
ENV = 'staging'
# 芋道管理系统/测试环境
VUE_APP_BASE_API = 'http://127.0.0.1:48080'

View File

@ -8,6 +8,7 @@
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"build:demo1024": "vue-cli-service build --mode demo1024",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src"
},

View File

@ -8,3 +8,19 @@ export function exportHtml() {
responseType: 'blob'
})
}
export function exportWord() {
return request({
url: '/infra/db-doc/export-word',
method: 'get',
responseType: 'blob'
})
}
export function exportMarkdown() {
return request({
url: '/infra/db-doc/export-markdown',
method: 'get',
responseType: 'blob'
})
}

View File

@ -0,0 +1,18 @@
import request from '@/utils/request'
// 删除文件
export function deleteFile(id) {
return request({
url: '/infra/file/delete?id=' + id,
method: 'delete'
})
}
// 获得文件分页
export function getFilePage(query) {
return request({
url: '/infra/file/page',
method: 'get',
params: query
})
}

View File

@ -46,7 +46,7 @@ export function addDept(data) {
export function updateDept(data) {
return request({
url: '/system/dept/update',
method: 'post',
method: 'put',
data: data
})
}
@ -55,6 +55,6 @@ export function updateDept(data) {
export function delDept(id) {
return request({
url: '/system/dept/delete?id=' + id,
method: 'post'
method: 'delete'
})
}

View File

@ -38,7 +38,7 @@ export function addData(data) {
export function updateData(data) {
return request({
url: '/system/dict-data/update',
method: 'post',
method: 'put',
data: data
})
}
@ -47,7 +47,7 @@ export function updateData(data) {
export function delData(dictCode) {
return request({
url: '/system/dict-data/delete?id=' + dictCode,
method: 'post'
method: 'delete'
})
}

View File

@ -30,7 +30,7 @@ export function addType(data) {
export function updateType(data) {
return request({
url: '/system/dict-type/update',
method: 'post',
method: 'put',
data: data
})
}
@ -39,7 +39,7 @@ export function updateType(data) {
export function delType(dictId) {
return request({
url: '/system/dict-type/delete?id=' + dictId,
method: 'post'
method: 'delete'
})
}

View File

@ -38,7 +38,7 @@ export function addMenu(data) {
export function updateMenu(data) {
return request({
url: '/system/menu/update',
method: 'post',
method: 'put',
data: data
})
}
@ -47,6 +47,6 @@ export function updateMenu(data) {
export function delMenu(id) {
return request({
url: '/system/menu/delete?id=' + id,
method: 'post'
method: 'delete'
})
}

View File

@ -30,7 +30,7 @@ export function addNotice(data) {
export function updateNotice(data) {
return request({
url: '/system/notice/update',
method: 'post',
method: 'put',
data: data
})
}
@ -39,6 +39,6 @@ export function updateNotice(data) {
export function delNotice(noticeId) {
return request({
url: '/system/notice/delete?id=' + noticeId,
method: 'post'
method: 'delete'
})
}

View File

@ -38,7 +38,7 @@ export function addPost(data) {
export function updatePost(data) {
return request({
url: '/system/post/update',
method: 'post',
method: 'put',
data: data
})
}
@ -47,7 +47,7 @@ export function updatePost(data) {
export function delPost(postId) {
return request({
url: '/system/post/delete?id=' + postId,
method: 'post'
method: 'delete'
})
}

View File

@ -38,7 +38,7 @@ export function addRole(data) {
export function updateRole(data) {
return request({
url: '/system/role/update',
method: 'post',
method: 'put',
data: data
})
}
@ -51,7 +51,7 @@ export function changeRoleStatus(id, status) {
}
return request({
url: '/system/role/update-status',
method: 'post',
method: 'put',
data: data
})
}
@ -60,7 +60,7 @@ export function changeRoleStatus(id, status) {
export function delRole(roleId) {
return request({
url: '/system/role/delete?id=' + roleId,
method: 'post'
method: 'delete'
})
}

View File

@ -0,0 +1,52 @@
import request from '@/utils/request'
// 创建短信渠道
export function createSmsChannel(data) {
return request({
url: '/system/sms-channel/create',
method: 'post',
data: data
})
}
// 更新短信渠道
export function updateSmsChannel(data) {
return request({
url: '/system/sms-channel/update',
method: 'put',
data: data
})
}
// 删除短信渠道
export function deleteSmsChannel(id) {
return request({
url: '/system/sms-channel/delete?id=' + id,
method: 'delete'
})
}
// 获得短信渠道
export function getSmsChannel(id) {
return request({
url: '/system/sms-channel/get?id=' + id,
method: 'get'
})
}
// 获得短信渠道分页
export function getSmsChannelPage(query) {
return request({
url: '/system/sms-channel/page',
method: 'get',
params: query
})
}
// 获得短信渠道精简列表
export function getSimpleSmsChannels() {
return request({
url: '/system/sms-channel/list-all-simple',
method: 'get',
})
}

View File

@ -0,0 +1,20 @@
import request from '@/utils/request'
// 获得短信日志分页
export function getSmsLogPage(query) {
return request({
url: '/system/sms-log/page',
method: 'get',
params: query
})
}
// 导出短信日志 Excel
export function exportSmsLogExcel(query) {
return request({
url: '/system/sms-log/export-excel',
method: 'get',
params: query,
responseType: 'blob'
})
}

View File

@ -0,0 +1,64 @@
import request from '@/utils/request'
// 创建短信模板
export function createSmsTemplate(data) {
return request({
url: '/system/sms-template/create',
method: 'post',
data: data
})
}
// 更新短信模板
export function updateSmsTemplate(data) {
return request({
url: '/system/sms-template/update',
method: 'put',
data: data
})
}
// 删除短信模板
export function deleteSmsTemplate(id) {
return request({
url: '/system/sms-template/delete?id=' + id,
method: 'delete'
})
}
// 获得短信模板
export function getSmsTemplate(id) {
return request({
url: '/system/sms-template/get?id=' + id,
method: 'get'
})
}
// 获得短信模板分页
export function getSmsTemplatePage(query) {
return request({
url: '/system/sms-template/page',
method: 'get',
params: query
})
}
// 创建短信模板
export function sendSms(data) {
return request({
url: '/system/sms-template/send-sms',
method: 'post',
data: data
})
}
// 导出短信模板 Excel
export function exportSmsTemplateExcel(query) {
return request({
url: '/system/sms-template/export-excel',
method: 'get',
params: query,
responseType: 'blob'
})
}

View File

@ -31,7 +31,7 @@ export function addUser(data) {
export function updateUser(data) {
return request({
url: '/system/user/update',
method: 'post',
method: 'put',
data: data
})
}
@ -62,7 +62,7 @@ export function resetUserPwd(id, password) {
}
return request({
url: '/system/user/update-password',
method: 'post',
method: 'put',
data: data
})
}
@ -75,7 +75,7 @@ export function changeUserStatus(id, status) {
}
return request({
url: '/system/user/update-status',
method: 'post',
method: 'put',
data: data
})
}
@ -83,7 +83,7 @@ export function changeUserStatus(id, status) {
// 查询用户个人信息
export function getUserProfile() {
return request({
url: '/system/user/profile',
url: '/system/user/profile/get',
method: 'get'
})
}
@ -91,7 +91,7 @@ export function getUserProfile() {
// 修改用户个人信息
export function updateUserProfile(data) {
return request({
url: '/system/user/profile',
url: '/system/user/profile/update',
method: 'put',
data: data
})
@ -104,9 +104,9 @@ export function updateUserPwd(oldPassword, newPassword) {
newPassword
}
return request({
url: '/system/user/profile/updatePwd',
url: '/system/user/profile/update-password',
method: 'put',
params: data
data: data
})
}
@ -114,7 +114,7 @@ export function updateUserPwd(oldPassword, newPassword) {
export function uploadAvatar(data) {
return request({
url: '/system/user/profile/avatar',
method: 'post',
method: 'put',
data: data
})
}

View File

@ -25,7 +25,10 @@ import {
download,
handleTree,
downloadExcel,
downloadZip
downloadWord,
downloadZip,
downloadHtml,
downloadMarkdown,
} from "@/utils/ruoyi";
import Pagination from "@/components/Pagination";
// 自定义表格工具扩展
@ -48,6 +51,9 @@ Vue.prototype.getDictDataLabel = getDictDataLabel
Vue.prototype.DICT_TYPE = DICT_TYPE
Vue.prototype.download = download
Vue.prototype.downloadExcel = downloadExcel
Vue.prototype.downloadWord = downloadWord
Vue.prototype.downloadHtml = downloadHtml
Vue.prototype.downloadMarkdown = downloadMarkdown
Vue.prototype.downloadZip = downloadZip
Vue.prototype.handleTree = handleTree

View File

@ -53,7 +53,7 @@ const user = {
getInfo(state.token).then(res => {
res = res.data; // 读取 data 数据
const user = res.user
const avatar = user.avatar === "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
const avatar = user.avatar === "" ? require("@/assets/images/profile.jpg") : user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)

View File

@ -17,6 +17,10 @@ export const DICT_TYPE = {
SYS_OPERATE_TYPE: 'sys_operate_type',
SYS_LOGIN_RESULT: 'sys_login_result',
SYS_CONFIG_TYPE: 'sys_config_type',
SYS_SMS_CHANNEL_CODE: 'sys_sms_channel_code',
SYS_SMS_TEMPLATE_TYPE: 'sys_sms_template_type',
SYS_SMS_SEND_STATUS: 'sys_sms_send_status',
SYS_SMS_RECEIVE_STATUS: 'sys_sms_receive_status',
INF_REDIS_TIMEOUT_TYPE: 'inf_redis_timeout_type',
INF_JOB_STATUS: 'inf_job_status',

View File

@ -73,6 +73,18 @@ service.interceptors.response.use(res => {
type: 'error'
})
return Promise.reject(new Error(msg))
} else if (code === 901) {
Message({
type: 'error',
duration: 0,
dangerouslyUseHTMLString: true,
message: '<div>演示模式,不发进行写操作</div>'
+ '<div> &nbsp; </div>'
+ '<div>参考 https://www.iocoder.cn/Yudao/build-debugger-environment 教程</div>'
+ '<div> &nbsp; </div>'
+ '<div>5 分钟搭建本地环境</div>',
})
return Promise.reject(new Error(msg))
} else if (code !== 200) {
Notification.error({
title: msg

View File

@ -120,11 +120,26 @@ export function downloadExcel(data, fileName) {
download0(data, fileName, 'application/vnd.ms-excel');
}
// 下载 Word 方法
export function downloadWord(data, fileName) {
download0(data, fileName, 'application/msword');
}
// 下载 Zip 方法
export function downloadZip(data, fileName) {
download0(data, fileName, 'application/zip');
}
// 下载 Html 方法
export function downloadHtml(data, fileName) {
download0(data, fileName, 'text/html');
}
// 下载 Markdown 方法
export function downloadMarkdown(data, fileName) {
download0(data, fileName, 'text/markdown');
}
function download0(data, fileName, mineType) {
// 创建 blob
let blob = new Blob([data], {type: mineType});

View File

@ -2,24 +2,12 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="参数名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入参数名称"
clearable
size="small"
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.name" placeholder="请输入参数名称" clearable size="small" style="width: 240px"
@keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="参数键名" prop="key">
<el-input
v-model="queryParams.key"
placeholder="请输入参数键名"
clearable
size="small"
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.key" placeholder="请输入参数键名" clearable size="small" style="width: 240px"
@keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="系统内置" prop="type">
<el-select v-model="queryParams.type" placeholder="系统内置" clearable size="small">
@ -56,7 +44,7 @@
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['infra:config:add']"
v-hasPermi="['infra:config:create']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
@ -95,31 +83,15 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['infra:config:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['infra:config:remove']"
>删除</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['infra:config:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['infra:config:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNo"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize" @pagination="getList"/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>

View File

@ -0,0 +1,202 @@
<template>
<div class="app-container">
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="文件路径" prop="id">
<el-input v-model="queryParams.id" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="文件类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择文件类型" clearable size="small">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">上传文件</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="文件路径" align="center" prop="id" width="300" />
<el-table-column label="文件类型" align="center" prop="type" width="80" />
<el-table-column label="文件内容" align="center" prop="content">
<template slot-scope="scope">
<img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'"
width="200px" :src="getFileUrl + scope.row.id">
<i v-else>非图片无法预览</i>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['infra:file:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
<el-upload ref="upload" :limit="1" accept=".jpg, .png, .gif" :auto-upload="false" drag
:headers="upload.headers" :action="upload.url" :data="upload.data" :disabled="upload.isUploading"
:on-change="handleFileChange"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess">
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处 <em>点击上传</em>
</div>
<div class="el-upload__tip" style="color:red" slot="tip">提示仅允许导入 jpgpnggif 格式文件</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitFileForm"> </el-button>
<el-button @click="upload.open = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { deleteFile, getFilePage } from "@/api/infra/file";
import {getToken} from "@/utils/auth";
export default {
name: "File",
data() {
return {
getFileUrl: process.env.VUE_APP_BASE_API + '/api/infra/file/get/',
//
loading: true,
//
showSearch: true,
//
total: 0,
//
list: [],
//
title: "",
dateRangeCreateTime: [],
//
queryParams: {
pageNo: 1,
pageSize: 10,
id: null,
type: null,
},
//
upload: {
open: false, //
title: "", //
isUploading: false, //
url: process.env.VUE_APP_BASE_API + '/api/' + "/infra/file/upload", //
headers: { Authorization: "Bearer " + getToken() }, //
data: {} //
},
};
},
created() {
this.getList();
},
methods: {
/** 查询列表 */
getList() {
this.loading = true;
//
let params = {...this.queryParams};
this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
//
getFilePage(params).then(response => {
this.list = response.data.list;
this.total = response.data.total;
this.loading = false;
});
},
/** 取消按钮 */
cancel() {
this.open = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
content: undefined,
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNo = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRangeCreateTime = [];
this.resetForm("queryForm");
this.handleQuery();
},
/** 新增按钮操作 */
handleAdd() {
this.upload.open = true;
this.upload.title = "上传文件";
},
/** 处理上传的文件发生变化 */
handleFileChange(file, fileList) {
this.upload.data.path = file.name;
},
/** 处理文件上传中 */
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true; //
},
/** 发起文件上窜 */
submitFileForm() {
this.$refs.upload.submit();
},
/** 文件上传成功处理 */
handleFileSuccess(response, file, fileList) {
//
this.upload.open = false;
this.upload.isUploading = false;
this.$refs.upload.clearFiles();
//
this.msgSuccess("上传成功");
this.getList();
},
/** 删除按钮操作 */
handleDelete(row) {
const id = row.id;
this.$confirm('是否确认删除文件编号为"' + id + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return deleteFile(id);
}).then(() => {
this.getList();
this.msgSuccess("删除成功");
})
},
}
};
</script>

View File

@ -2,22 +2,11 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item label="部门名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入部门名称"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.name" placeholder="请输入部门名称" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable size="small">
<el-option
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
<el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
</el-select>
</el-form-item>
<el-form-item>
@ -28,24 +17,13 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:dept:add']"
>新增</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:dept:create']">新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
v-loading="loading"
:data="deptList"
row-key="id"
default-expand-all
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table v-loading="loading" :data="deptList" row-key="id" default-expand-all
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column prop="name" label="部门名称" width="260"></el-table-column>
<el-table-column prop="sort" label="排序" width="200"></el-table-column>
<el-table-column prop="status" label="状态" :formatter="statusFormat" width="100"></el-table-column>
@ -56,28 +34,12 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:dept:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-plus"
@click="handleAdd(scope.row)"
v-hasPermi="['system:dept:add']"
>新增</el-button>
<el-button
v-if="scope.row.parentId !== 0"
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:dept:remove']"
>删除</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:dept:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-plus" @click="handleAdd(scope.row)"
v-hasPermi="['system:dept:create']">新增</el-button>
<el-button v-if="scope.row.parentId !== 0" size="mini" type="text" icon="el-icon-delete"
@click="handleDelete(scope.row)" v-hasPermi="['system:dept:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
@ -119,11 +81,8 @@
<el-col :span="12">
<el-form-item label="部门状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="parseInt(dict.value)"
>{{dict.label}}</el-radio>
<el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>

View File

@ -3,31 +3,15 @@
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="字典名称" prop="dictType">
<el-select v-model="queryParams.dictType" size="small">
<el-option
v-for="item in typeOptions"
:key="item.id"
:label="item.name"
:value="item.type"
/>
<el-option v-for="item in typeOptions" :key="item.id" :label="item.name" :value="item.type"/>
</el-select>
</el-form-item>
<el-form-item label="字典标签" prop="label">
<el-input
v-model="queryParams.label"
placeholder="请输入字典标签"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.label" placeholder="请输入字典标签" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="数据状态" clearable size="small">
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
<el-option v-for="dict in statusOptions" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item>
@ -38,22 +22,12 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:dict:add']"
>新增</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:dict:create']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:dict:export']"
>导出</el-button>
<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:dict:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -72,31 +46,16 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:dict:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:dict:remove']"
>删除</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:dict:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:dict:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNo"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
@ -115,11 +74,7 @@
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="parseInt(dict.value)"
>{{dict.label}}</el-radio>
<el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">

View File

@ -2,52 +2,19 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="字典名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入字典名称"
clearable
size="small"
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.name" placeholder="请输入字典名称" clearable size="small" style="width: 240px" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="字典类型" prop="type">
<el-input
v-model="queryParams.type"
placeholder="请输入字典类型"
clearable
size="small"
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.type" placeholder="请输入字典类型" clearable size="small" style="width: 240px" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="字典状态"
clearable
size="small"
style="width: 240px"
>
<el-option
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
<el-select v-model="queryParams.status" placeholder="字典状态" clearable size="small" style="width: 240px">
<el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRangeCreateTime"
size="small"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
<el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd" type="daterange"
range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@ -57,22 +24,12 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:dict:add']"
>新增</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:dict:create']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:dict:export']"
>导出</el-button>
<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:dict:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -96,31 +53,16 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:dict:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:dict:remove']"
>删除</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:dict:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:dict:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNo"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
@ -133,11 +75,7 @@
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="parseInt(dict.value)"
>{{dict.label}}</el-radio>
<el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">

View File

@ -2,56 +2,22 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="登录地址" prop="userIp">
<el-input
v-model="queryParams.userIp"
placeholder="请输入登录地址"
clearable
style="width: 240px;"
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.userIp" placeholder="请输入登录地址" clearable style="width: 240px;" size="small"
@keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="用户名称" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名称"
clearable
style="width: 240px;"
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.username" placeholder="请输入用户名称" clearable style="width: 240px;" size="small"
@keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="结果"
clearable
size="small"
style="width: 240px"
>
<el-option
:key="true"
label="成功"
:value="true"
/>
<el-option
:key="false"
label="失败"
:value="false"
/>
<el-select v-model="queryParams.status" placeholder="结果" clearable size="small" style="width: 240px">
<el-option :key="true" label="成功" :value="true"/>
<el-option :key="false" label="失败" :value="false"/>
</el-select>
</el-form-item>
<el-form-item label="登录时间">
<el-date-picker
v-model="dateRange"
size="small"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
<el-date-picker v-model="dateRange" size="small" style="width: 240px" value-format="yyyy-MM-dd"
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@ -61,13 +27,8 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="warning"
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:login-log:export']"
>导出</el-button>
<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:login-log:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -76,7 +37,7 @@
<el-table-column label="访问编号" align="center" prop="id" />
<el-table-column label="日志类型" align="center" prop="logType">
<template slot-scope="scope">
<span>{{ scope.row.logType === 1 ? '登录' : '退出' }}</span>
<span>{{ scope.row.logType === 100 ? '登录' : '退出' }}</span>
</template>
</el-table-column>
<el-table-column label="用户名称" align="center" prop="username" />
@ -95,13 +56,8 @@
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNo"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
</div>
</template>

View File

@ -2,22 +2,11 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item label="菜单名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入菜单名称"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.name" placeholder="请输入菜单名称" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable size="small">
<el-option
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
<el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
</el-select>
</el-form-item>
<el-form-item>
@ -28,23 +17,14 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:menu:add']"
>新增</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:menu:create']">新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
v-loading="loading"
:data="menuList"
row-key="id"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table v-loading="loading" :data="menuList" row-key="id"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="200"></el-table-column>
<el-table-column prop="icon" label="图标" align="center" width="100">
<template slot-scope="scope">
@ -62,26 +42,12 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:menu:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-plus"
@click="handleAdd(scope.row)"
v-hasPermi="['system:menu:add']"
>新增</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:menu:remove']"
>删除</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:menu:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-plus" @click="handleAdd(scope.row)"
v-hasPermi="['system:menu:create']">新增</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:menu:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
@ -92,43 +58,25 @@
<el-row>
<el-col :span="24">
<el-form-item label="上级菜单">
<treeselect
v-model="form.parentId"
:options="menuOptions"
:normalizer="normalizer"
:show-count="true"
placeholder="选择上级菜单"
/>
<treeselect v-model="form.parentId" :options="menuOptions" :normalizer="normalizer" :show-count="true"
placeholder="选择上级菜单"/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio
v-for="dict in menuTypeDictDatas"
:key="parseInt(dict.value)"
:label="parseInt(dict.value)"
>{{dict.label}}</el-radio>
<el-radio v-for="dict in menuTypeDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item v-if="form.type != '3'" label="菜单图标">
<el-popover
placement="bottom-start"
width="460"
trigger="click"
@show="$refs['iconSelect'].reset()"
>
<el-popover placement="bottom-start" width="460" trigger="click" @show="$refs['iconSelect'].reset()">
<IconSelect ref="iconSelect" @selected="selected" />
<el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
<svg-icon
v-if="form.icon"
slot="prefix"
:icon-class="form.icon"
class="el-input__icon"
style="height: 32px;width: 16px;"
/>
<svg-icon v-if="form.icon" slot="prefix" :icon-class="form.icon" class="el-input__icon"
style="height: 32px;width: 16px;"/>
<i v-else slot="prefix" class="el-icon-search el-input__icon" />
</el-input>
</el-popover>

View File

@ -2,31 +2,14 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="公告标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入公告标题"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.title" placeholder="请输入公告标题" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="操作人员" prop="createBy">
<el-input
v-model="queryParams.createBy"
placeholder="请输入操作人员"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.createBy" placeholder="请输入操作人员" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="queryParams.type" placeholder="公告类型" clearable size="small">
<el-option
v-for="dict in noticeTypeDictDatas"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
<el-option v-for="dict in noticeTypeDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
</el-select>
</el-form-item>
<el-form-item>
@ -37,39 +20,16 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:notice:add']"
>新增</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:notice:create']"s>新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="noticeList">
<el-table-column label="序号" align="center" prop="id" width="100" />
<el-table-column
label="公告标题"
align="center"
prop="title"
:show-overflow-tooltip="true"
/>
<el-table-column
label="公告类型"
align="center"
prop="type"
:formatter="typeFormat"
width="100"
/>
<el-table-column
label="状态"
align="center"
prop="status"
:formatter="statusFormat"
width="100"
/>
<el-table-column label="公告标题" align="center" prop="title" :show-overflow-tooltip="true"/>
<el-table-column label="公告类型" align="center" prop="type" :formatter="typeFormat" width="100"/>
<el-table-column label="状态" align="center" prop="status" :formatter="statusFormat" width="100"/>
<el-table-column label="创建者" align="center" prop="createBy" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="100">
<template slot-scope="scope">
@ -78,31 +38,16 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:notice:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:notice:remove']"
>删除</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:notice:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:notice:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNo"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 添加或修改公告对话框 -->
<el-dialog :title="title" :visible.sync="open" width="780px" append-to-body>

View File

@ -2,72 +2,28 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="系统模块" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入系统模块"
clearable
style="width: 240px;"
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable style="width: 240px;" size="small"
@keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="操作人员" prop="operName">
<el-input
v-model="queryParams.operName"
placeholder="请输入操作人员"
clearable
style="width: 240px;"
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.operName" placeholder="请输入操作人员" clearable style="width: 240px;" size="small"
@keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="操作类型"
clearable
size="small"
style="width: 240px"
>
<el-option
v-for="dict in this.getDictDatas(DICT_TYPE.SYS_OPERATE_TYPE)"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
<el-select v-model="queryParams.type" placeholder="操作类型" clearable size="small" style="width: 240px">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_OPERATE_TYPE)" :key="parseInt(dict.value)"
:label="dict.label" :value="parseInt(dict.value)"/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.success"
placeholder="操作状态"
clearable
size="small"
style="width: 240px"
>
<el-option
:key="true"
label="成功"
:value="true"
/>
<el-option
:key="false"
label="失败"
:value="false"
/>
<el-select v-model="queryParams.success" placeholder="操作状态" clearable size="small" style="width: 240px">
<el-option :key="true" label="成功" :value="true"/>
<el-option :key="false" label="失败" :value="false"/>
</el-select>
</el-form-item>
<el-form-item label="操作时间">
<el-date-picker
v-model="dateRange"
size="small"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
<el-date-picker v-model="dateRange" size="small" style="width: 240px" value-format="yyyy-MM-dd"
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@ -77,13 +33,8 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="warning"
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:config:export']"
>导出</el-button>
<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:operate-log:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -212,7 +163,6 @@ export default {
businessType: undefined,
status: undefined
},
};
},
created() {

View File

@ -2,31 +2,14 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="岗位编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入岗位编码"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.code" placeholder="请输入岗位编码" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="岗位名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入岗位名称"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.name" placeholder="请输入岗位名称" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="岗位状态" clearable size="small">
<el-option
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
<el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
</el-select>
</el-form-item>
<el-form-item>
@ -37,22 +20,12 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:post:add']"
>新增</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:post:create']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:post:export']"
>导出</el-button>
<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:post:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -70,31 +43,16 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:post:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:post:remove']"
>删除</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:post:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:post:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNo"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 添加或修改岗位对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
@ -110,11 +68,8 @@
</el-form-item>
<el-form-item label="岗位状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="parseInt(dict.value)"
>{{dict.label}}</el-radio>
<el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">

View File

@ -2,52 +2,21 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" v-show="showSearch" :inline="true">
<el-form-item label="角色名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入角色名称"
clearable
size="small"
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.name" placeholder="请输入角色名称" clearable size="small" style="width: 240px"
@keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="角色标识" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入角色标识"
clearable
size="small"
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
<el-input v-model="queryParams.code" placeholder="请输入角色标识" clearable size="small" style="width: 240px"
@keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="角色状态"
clearable
size="small"
style="width: 240px"
>
<el-option
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
<el-select v-model="queryParams.status" placeholder="角色状态" clearable size="small" style="width: 240px">
<el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
size="small"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
<el-date-picker v-model="dateRange" size="small" style="width: 240px" value-format="yyyy-MM-dd" type="daterange"
range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@ -57,22 +26,12 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:role:add']"
>新增</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:role:create']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:role:export']"
>导出</el-button>
<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:role:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -85,12 +44,7 @@
<el-table-column label="显示顺序" prop="sort" width="100" />
<el-table-column label="状态" align="center" width="100">
<template slot-scope="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(scope.row)"
></el-switch>
<el-switch v-model="scope.row.status" :active-value="0" :inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
@ -100,45 +54,20 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:role:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-circle-check"
@click="handleMenu(scope.row)"
v-hasPermi="['system:permission:assign-role-menu']"
>菜单权限</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-circle-check"
@click="handleDataScope(scope.row)"
v-hasPermi="['system:permission:assign-role-data-scope']"
>数据权限</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:role:remove']"
>删除</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:role:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-circle-check" @click="handleMenu(scope.row)"
v-hasPermi="['system:permission:assign-role-menu']">菜单权限</el-button>
<el-button size="mini" type="text" icon="el-icon-circle-check" @click="handleDataScope(scope.row)"
v-hasPermi="['system:permission:assign-role-data-scope']">数据权限</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:role:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 添加或修改角色配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>

View File

@ -0,0 +1,542 @@
<template>
<div class="app-container">
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="短信签名" prop="signature">
<el-input v-model="queryParams.signature" placeholder="请输入短信签名" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="启用状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable size="small">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:sms-channel:create']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:sms-channel:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="短信签名" align="center" prop="signature" />
<el-table-column label="渠道编码" align="center" prop="code">
<template slot-scope="scope">
<span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.code) }}</span>
</template>
</el-table-column>>
<el-table-column label="启用状态" align="center" prop="status">
<template slot-scope="scope">
<span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
</template>
</el-table-column>>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="短信 API 的账号" align="center" prop="apiKey" />
<el-table-column label="短信 API 的秘钥" align="center" prop="apiSecret" />
<el-table-column label="短信发送回调 URL" align="center" prop="callbackUrl" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:sms-channel:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:sms-channel:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="短信签名" prop="signature">
<el-input v-model="form.signature" placeholder="请输入短信签名" />
</el-form-item>
<el-form-item label="渠道编码" prop="code">
<el-input v-model="form.code" placeholder="请输入渠道编码" />
</el-form-item>
<el-form-item label="启用状态">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
:key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="短信 API 的账号" prop="apiKey">
<el-input v-model="form.apiKey" placeholder="请输入短信 API 的账号" />
</el-form-item>
<el-form-item label="短信 API 的秘钥" prop="apiSecret">
<el-input v-model="form.apiSecret" placeholder="请输入短信 API 的秘钥" />
</el-form-item>
<el-form-item label="短信发送回调 URL" prop="callbackUrl">
<el-input v-model="form.callbackUrl" placeholder="请输入短信发送回调 URL" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { createSmsChannel, updateSmsChannel, deleteSmsChannel, getSmsChannel, getSmsChannelPage,
getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
export default {
name: "SmsChannel",
components: {
},
data() {
return {
//
loading: true,
//
showSearch: true,
//
total: 0,
//
list: [],
//
title: "",
//
open: false,
dateRangeCreateTime: [],
//
queryParams: {
pageNo: 1,
pageSize: 10,
signature: null,
status: null,
},
//
form: {},
//
rules: {
signature: [{ required: true, message: "短信签名不能为空", trigger: "blur" }],
code: [{ required: true, message: "渠道编码不能为空", trigger: "blur" }],
status: [{ required: true, message: "启用状态不能为空", trigger: "blur" }],
apiKey: [{ required: true, message: "短信 API 的账号不能为空", trigger: "blur" }],
},
};
},
created() {
this.getList();
},
methods: {
/** 查询列表 */
getList() {
this.loading = true;
//
let params = {...this.queryParams};
this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
//
getSmsChannelPage(params).then(response => {
this.list = response.data.list;
this.total = response.data.total;
this.loading = false;
});
},
/** 取消按钮 */
cancel() {
this.open = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
id: undefined,
signature: undefined,
code: undefined,
status: undefined,
remark: undefined,
apiKey: undefined,
apiSecret: undefined,
callbackUrl: undefined,
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNo = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRangeCreateTime = [];
this.resetForm("queryForm");
this.handleQuery();
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加短信渠道";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id;
getSmsChannel(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改短信渠道";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (!valid) {
return;
}
//
if (this.form.id != null) {
updateSmsChannel(this.form).then(response => {
this.msgSuccess("修改成功");
this.open = false;
this.getList();
});
return;
}
//
createSmsChannel(this.form).then(response => {
this.msgSuccess("新增成功");
this.open = false;
this.getList();
});
});
},
/** 删除按钮操作 */
handleDelete(row) {
const id = row.id;
this.$confirm('是否确认删除短信渠道编号为"' + id + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return deleteSmsChannel(id);
}).then(() => {
this.getList();
this.msgSuccess("删除成功");
})
},
/** 导出按钮操作 */
handleExport() {
//
let params = {...this.queryParams};
params.pageNo = undefined;
params.pageSize = undefined;
this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
//
this.$confirm('是否确认导出所有短信渠道数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return exportSmsChannelExcel(params);
}).then(response => {
this.downloadExcel(response, '短信渠道.xls');
})
}
}
};
</script><template>
<div class="app-container">
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="短信签名" prop="signature">
<el-input v-model="queryParams.signature" placeholder="请输入短信签名" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="启用状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable size="small">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:sms-channel:create']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:sms-channel:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="短信签名" align="center" prop="signature" />
<el-table-column label="渠道编码" align="center" prop="code">
<template slot-scope="scope">
<span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.code) }}</span>
</template>
</el-table-column>>
<el-table-column label="启用状态" align="center" prop="status">
<template slot-scope="scope">
<span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
</template>
</el-table-column>>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:sms-channel:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:sms-channel:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="130px">
<el-form-item label="短信签名" prop="signature">
<el-input v-model="form.signature" placeholder="请输入短信签名" />
</el-form-item>
<el-form-item label="渠道编码" prop="code">
<el-select v-model="form.code" placeholder="请选择渠道编码" :disabled="form.id > 0">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_CHANNEL_CODE)"
:key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="启用状态">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
:key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="短信 API 的账号" prop="apiKey">
<el-input v-model="form.apiKey" placeholder="请输入短信 API 的账号" />
</el-form-item>
<el-form-item v-if="form.code !== 'YUN_PIAN'" label="短信 API 的秘钥" prop="apiSecret">
<el-input v-model="form.apiSecret" placeholder="请输入短信 API 的秘钥" />
</el-form-item>
<el-form-item label="短信发送回调 URL" prop="callbackUrl">
<el-input v-model="form.callbackUrl" placeholder="请输入短信发送回调 URL" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { createSmsChannel, updateSmsChannel, deleteSmsChannel, getSmsChannel, getSmsChannelPage } from "@/api/system/sms/smsChannel";
export default {
name: "SmsChannel",
components: {
},
data() {
return {
//
loading: true,
//
showSearch: true,
//
total: 0,
//
list: [],
//
title: "",
//
open: false,
dateRangeCreateTime: [],
//
queryParams: {
pageNo: 1,
pageSize: 10,
signature: null,
status: null,
},
//
form: {},
//
apiKeyEnableChannelCodes: ['YUN_PIAN'],
rules: {
signature: [{ required: true, message: "短信签名不能为空", trigger: "blur" }],
code: [{ required: true, message: "渠道编码不能为空", trigger: "blur" }],
status: [{ required: true, message: "启用状态不能为空", trigger: "blur" }],
apiKey: [{ required: true, message: "短信 API 的账号不能为空", trigger: "blur" }],
apiSecret: [{ required: true, message: "短信 API 的秘钥不能为空", trigger: "blur" }],
}
};
},
created() {
this.getList();
},
methods: {
/** 查询列表 */
getList() {
this.loading = true;
//
let params = {...this.queryParams};
this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
//
getSmsChannelPage(params).then(response => {
this.list = response.data.list;
this.total = response.data.total;
this.loading = false;
});
},
/** 取消按钮 */
cancel() {
this.open = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
id: undefined,
signature: undefined,
code: undefined,
status: undefined,
remark: undefined,
apiKey: undefined,
apiSecret: undefined,
callbackUrl: undefined,
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNo = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRangeCreateTime = [];
this.resetForm("queryForm");
this.handleQuery();
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加短信渠道";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id;
getSmsChannel(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改短信渠道";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (!valid) {
return;
}
//
if (this.form.id != null) {
updateSmsChannel(this.form).then(response => {
this.msgSuccess("修改成功");
this.open = false;
this.getList();
});
return;
}
//
createSmsChannel(this.form).then(response => {
this.msgSuccess("新增成功");
this.open = false;
this.getList();
});
});
},
/** 删除按钮操作 */
handleDelete(row) {
const id = row.id;
this.$confirm('是否确认删除短信渠道编号为"' + id + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return deleteSmsChannel(id);
}).then(() => {
this.getList();
this.msgSuccess("删除成功");
})
},
/** 导出按钮操作 */
handleExport() {
//
let params = {...this.queryParams};
params.pageNo = undefined;
params.pageSize = undefined;
this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
//
this.$confirm('是否确认导出所有短信渠道数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return exportSmsChannelExcel(params);
}).then(response => {
this.downloadExcel(response, '短信渠道.xls');
})
}
}
};
</script>

View File

@ -0,0 +1,297 @@
<template>
<div class="app-container">
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="手机号" prop="mobile">
<el-input v-model="queryParams.mobile" placeholder="请输入手机号" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="短信渠道" prop="channelId">
<el-select v-model="queryParams.channelId" placeholder="请选择短信渠道" clearable size="small">
<el-option v-for="channel in channelOptions"
:key="channel.id" :value="channel.id"
:label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
</el-select>
</el-form-item>
<el-form-item label="模板编号" prop="templateId">
<el-input v-model="queryParams.templateId" placeholder="请输入模板编号" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="发送状态" prop="sendStatus">
<el-select v-model="queryParams.sendStatus" placeholder="请选择发送状态" clearable size="small">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_SEND_STATUS)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="发送时间">
<el-date-picker v-model="dateRangeSendTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
<el-form-item label="接收状态" prop="receiveStatus">
<el-select v-model="queryParams.receiveStatus" placeholder="请选择接收状态" clearable size="small">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_RECEIVE_STATUS)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="接收时间">
<el-date-picker v-model="dateRangeReceiveTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:sms-log:create']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:sms-log:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="手机号" align="center" prop="mobile" width="120">
<template slot-scope="scope">
<div>{{ scope.row.mobile }}</div>
<div v-if="scope.row.userType && scope.row.userId">
{{ getDictDataLabel(DICT_TYPE.USER_TYPE, scope.row.userType) + '(' + scope.row.userId + ')' }}
</div>
</template>
</el-table-column>
<el-table-column label="短信内容" align="center" prop="templateContent" width="300" />
<el-table-column label="发送状态" align="center" width="180">
<template slot-scope="scope">
<div>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_SEND_STATUS, scope.row.sendStatus) }}</div>
<div>{{ parseTime(scope.row.sendTime) }}</div>
</template>
</el-table-column>
<el-table-column label="接收状态" align="center" width="180">
<template slot-scope="scope">
<div>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_RECEIVE_STATUS, scope.row.receiveStatus) }}</div>
<div>{{ parseTime(scope.row.receiveTime) }}</div>
</template>
</el-table-column>
<el-table-column label="短信渠道" align="center" width="120">
<template slot-scope="scope">
<div>{{ formatChannelSignature(scope.row.channelId) }}</div>
<div>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}</div>
</template>
</el-table-column>
<el-table-column label="模板编号" align="center" prop="templateId" />
<el-table-column label="短信类型" align="center" prop="templateType">
<template slot-scope="scope">
<span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, scope.row.templateType) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row,scope.index)"
v-hasPermi="['system:sms-log:query']">详细</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 短信日志详细 -->
<el-dialog title="短信日志详细" :visible.sync="open" width="700px" append-to-body>
<el-form ref="form" :model="form" label-width="140px" size="mini">
<el-row>
<el-col :span="24">
<el-form-item label="日志主键:">{{ form.id }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="短信渠道:">
{{ formatChannelSignature(form.channelId) }}{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, form.channelCode) }}
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="短信模板:">
{{ form.templateId }} | {{ form.templateCode}} | {{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, form.templateType) }}
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="API 的模板编号:">{{ form.apiTemplateId }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="用户信息:">{{ form.mobile }}
<span v-if="form.userType && form.userId"> | {{ getDictDataLabel(DICT_TYPE.USER_TYPE, form.userType) }} | {{ form.userId }}</span>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="短信内容:">{{ form.templateContent }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="短信参数:">{{ form.templateParams }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="创建时间:">{{ parseTime(form.createTime) }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="发送状态:">{{ getDictDataLabel(DICT_TYPE.SYS_SMS_SEND_STATUS, form.sendStatus) }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="发送时间:">{{ parseTime(form.sendTime) }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="发送结果:">{{ form.sendCode }} | {{ form.sendMsg }}
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="API 发送结果:">{{ form.apiSendCode }} | {{ form.apiSendMsg }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="API 短信编号:">{{ form.apiSerialNo }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="API 请求编号:">{{ form.apiRequestId }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="接收状态:">{{ getDictDataLabel(DICT_TYPE.SYS_SMS_RECEIVE_STATUS, form.receiveStatus) }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="接收时间:">{{ parseTime(form.receiveTime) }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="API 接收结果:">{{ form.apiReceiveCode }} | {{ form.apiReceiveMsg }}
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="open = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getSmsLogPage, exportSmsLogExcel } from "@/api/system/sms/smsLog";
import { getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
export default {
name: "SmsLog",
components: {
},
data() {
return {
//
loading: true,
//
showSearch: true,
//
total: 0,
//
list: [],
//
title: "",
//
open: false,
dateRangeSendTime: [],
dateRangeReceiveTime: [],
//
form: {},
//
queryParams: {
pageNo: 1,
pageSize: 10,
channelId: null,
templateId: null,
mobile: null,
sendStatus: null,
receiveStatus: null,
},
//
channelOptions: [],
};
},
created() {
this.getList();
//
getSimpleSmsChannels().then(response => {
this.channelOptions = response.data;
})
},
methods: {
/** 查询列表 */
getList() {
this.loading = true;
//
let params = {...this.queryParams};
this.addBeginAndEndTime(params, this.dateRangeSendTime, 'sendTime');
this.addBeginAndEndTime(params, this.dateRangeReceiveTime, 'receiveTime');
//
getSmsLogPage(params).then(response => {
this.list = response.data.list;
this.total = response.data.total;
this.loading = false;
});
},
/** 取消按钮 */
cancel() {
this.open = false;
this.reset();
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNo = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRangeSendTime = [];
this.dateRangeReceiveTime = [];
this.resetForm("queryForm");
this.handleQuery();
},
/** 导出按钮操作 */
handleExport() {
//
let params = {...this.queryParams};
params.pageNo = undefined;
params.pageSize = undefined;
this.addBeginAndEndTime(params, this.dateRangeSendTime, 'sendTime');
this.addBeginAndEndTime(params, this.dateRangeReceiveTime, 'receiveTime');
//
this.$confirm('是否确认导出所有短信日志数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return exportSmsLogExcel(params);
}).then(response => {
this.downloadExcel(response, '短信日志.xls');
})
},
/** 详细按钮操作 */
handleView(row) {
this.open = true;
this.form = row;
},
/** 格式化短信渠道 */
formatChannelSignature(channelId) {
for (const channel of this.channelOptions) {
if (channel.id === channelId) {
return channel.signature;
}
}
return '找不到签名:' + channelId;
}
}
};
</script>

View File

@ -0,0 +1,405 @@
<template>
<div class="app-container">
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="150px">
<el-form-item label="短信类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择短信类型" clearable size="small">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="开启状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择开启状态" clearable size="small">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="模板编码" prop="code">
<el-input v-model="queryParams.code" placeholder="请输入模板编码" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="短信 API 的模板编号" prop="apiTemplateId">
<el-input v-model="queryParams.apiTemplateId" placeholder="请输入短信 API 的模板编号" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="短信渠道" prop="channelId">
<el-select v-model="queryParams.channelId" placeholder="请选择短信渠道" clearable size="small">
<el-option v-for="channel in channelOptions"
:key="channel.id" :value="channel.id"
:label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:sms-template:create']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:sms-template:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="模板编码" align="center" prop="code" />
<el-table-column label="模板名称" align="center" prop="name" />
<el-table-column label="模板内容" align="center" prop="content" width="300" />
<el-table-column label="短信类型" align="center" prop="type">
<template slot-scope="scope">
<span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, scope.row.type) }}</span>
</template>
</el-table-column>
<el-table-column label="开启状态" align="center" prop="status">
<template slot-scope="scope">
<span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="短信 API 的模板编号" align="center" prop="apiTemplateId" width="180" />
<el-table-column label="短信渠道" align="center" width="120">
<template slot-scope="scope">
<div>{{ formatChannelSignature(scope.row.channelId) }}</div>
<div>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}</div>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-share" @click="handleSendSms(scope.row)"
v-hasPermi="['system:sms-template:send-sms']">测试</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-hasPermi="['system:sms-template:update']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['system:sms-template:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="140px">
<el-form-item label="短信渠道编号" prop="channelId">
<el-select v-model="form.channelId" placeholder="请选择短信渠道编号">
<el-option v-for="channel in channelOptions"
:key="channel.id" :value="channel.id"
:label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
</el-select>
</el-form-item>
<el-form-item label="短信类型" prop="type">
<el-select v-model="form.type" placeholder="请选择短信类型">
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE)"
:key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
</el-select>
</el-form-item>
<el-form-item label="模板编号" prop="code">
<el-input v-model="form.code" placeholder="请输入模板编号" />
</el-form-item>
<el-form-item label="模板名称" prop="name">
<el-input v-model="form.name" placeholder="请输入模板名称" />
</el-form-item>
<el-form-item label="模板内容" prop="content">
<el-input type="textarea" v-model="form.content" placeholder="请输入模板内容" />
</el-form-item>
<el-form-item label="开启状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
:key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="短信 API 模板编号" prop="apiTemplateId">
<el-input v-model="form.apiTemplateId" placeholder="请输入短信 API 的模板编号" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 对话框(发送短信) -->
<el-dialog title="测试发送短信" :visible.sync="sendSmsOpen" width="500px" append-to-body>
<el-form ref="sendSmsForm" :model="sendSmsForm" :rules="sendSmsRules" label-width="140px">
<el-form-item label="模板内容" prop="content">
<el-input v-model="sendSmsForm.content" type="textarea" placeholder="请输入模板内容" readonly />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="sendSmsForm.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item v-for="param in sendSmsForm.params" :label="'参数 {' + param + '}'" :prop="'templateParams.' + param">
<el-input v-model="sendSmsForm.templateParams[param]" :placeholder="'请输入 ' + param + ' 参数'" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitSendSmsForm"> </el-button>
<el-button @click="cancelSendSms"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { createSmsTemplate, updateSmsTemplate, deleteSmsTemplate, getSmsTemplate, getSmsTemplatePage,
exportSmsTemplateExcel, sendSms } from "@/api/system/sms/smsTemplate";
import { getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
export default {
name: "SmsTemplate",
data() {
return {
//
loading: true,
//
showSearch: true,
//
total: 0,
//
list: [],
//
title: "",
//
open: false,
dateRangeCreateTime: [],
//
queryParams: {
pageNo: 1,
pageSize: 10,
type: null,
status: null,
code: null,
content: null,
apiTemplateId: null,
channelId: null,
},
//
form: {},
//
rules: {
type: [{ required: true, message: "短信类型不能为空", trigger: "change" }],
status: [{ required: true, message: "开启状态不能为空", trigger: "blur" }],
code: [{ required: true, message: "模板编码不能为空", trigger: "blur" }],
name: [{ required: true, message: "模板名称不能为空", trigger: "blur" }],
content: [{ required: true, message: "模板内容不能为空", trigger: "blur" }],
apiTemplateId: [{ required: true, message: "短信 API 的模板编号不能为空", trigger: "blur" }],
channelId: [{ required: true, message: "短信渠道编号不能为空", trigger: "change" }],
},
//
channelOptions: [],
//
sendSmsOpen: false,
sendSmsForm: {
params: [], //
},
sendSmsRules: {
mobile: [{ required: true, message: "手机不能为空", trigger: "blur" }],
templateCode: [{ required: true, message: "手机不能为空", trigger: "blur" }],
templateParams: { }
}
};
},
created() {
this.getList();
//
getSimpleSmsChannels().then(response => {
this.channelOptions = response.data;
})
},
methods: {
/** 查询列表 */
getList() {
this.loading = true;
//
let params = {...this.queryParams};
this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
//
getSmsTemplatePage(params).then(response => {
this.list = response.data.list;
this.total = response.data.total;
this.loading = false;
});
},
/** 取消按钮 */
cancel() {
this.open = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
id: undefined,
type: undefined,
status: undefined,
code: undefined,
name: undefined,
content: undefined,
remark: undefined,
apiTemplateId: undefined,
channelId: undefined,
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNo = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRangeCreateTime = [];
this.resetForm("queryForm");
this.handleQuery();
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加短信模板";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id;
getSmsTemplate(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改短信模板";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (!valid) {
return;
}
//
if (this.form.id != null) {
updateSmsTemplate(this.form).then(response => {
this.msgSuccess("修改成功");
this.open = false;
this.getList();
});
return;
}
//
createSmsTemplate(this.form).then(response => {
this.msgSuccess("新增成功");
this.open = false;
this.getList();
});
});
},
/** 删除按钮操作 */
handleDelete(row) {
const id = row.id;
this.$confirm('是否确认删除短信模板编号为"' + id + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return deleteSmsTemplate(id);
}).then(() => {
this.getList();
this.msgSuccess("删除成功");
})
},
/** 导出按钮操作 */
handleExport() {
//
let params = {...this.queryParams};
params.pageNo = undefined;
params.pageSize = undefined;
this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
//
this.$confirm('是否确认导出所有短信模板数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return exportSmsTemplateExcel(params);
}).then(response => {
this.downloadExcel(response, '短信模板.xls');
})
},
/** 发送短息按钮 */
handleSendSms(row) {
this.resetSendSms(row);
//
this.sendSmsForm.content = row.content;
this.sendSmsForm.params = row.params;
this.sendSmsForm.templateCode = row.code;
this.sendSmsForm.templateParams = row.params.reduce(function(obj, item) {
obj[item] = undefined;
return obj;
}, {});
// row rules
this.sendSmsRules.templateParams = row.params.reduce(function(obj, item) {
obj[item] = { required: true, message: '参数 ' + item + " 不能为空", trigger: "change" };
return obj;
}, {});
//
this.sendSmsOpen = true;
},
/** 重置发送短信的表单 */
resetSendSms() {
// row
this.sendSmsForm = {
content: undefined,
params: undefined,
mobile: undefined,
templateCode: undefined,
templateParams: {}
};
this.resetForm("sendSmsForm");
},
/** 取消发送短信 */
cancelSendSms() {
this.sendSmsOpen = false;
this.resetSendSms();
},
/** 提交按钮 */
submitSendSmsForm() {
this.$refs["sendSmsForm"].validate(valid => {
if (!valid) {
return;
}
//
sendSms(this.sendSmsForm).then(response => {
this.msgSuccess("提交发送成功!发送结果,见发送日志编号:" + response.data);
this.sendSmsOpen = false;
});
});
},
/** 格式化短信渠道 */
formatChannelSignature(channelId) {
for (const channel of this.channelOptions) {
if (channel.id === channelId) {
return channel.signature;
}
}
return '找不到签名:' + channelId;
}
}
};
</script>

File diff suppressed because it is too large Load Diff

View File

@ -13,11 +13,11 @@
<ul class="list-group list-group-striped">
<li class="list-group-item">
<svg-icon icon-class="user" />用户名称
<div class="pull-right">{{ user.userName }}</div>
<div class="pull-right">{{ user.username }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="phone" />手机号码
<div class="pull-right">{{ user.phonenumber }}</div>
<div class="pull-right">{{ user.mobile }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="email" />用户邮箱
@ -25,15 +25,19 @@
</li>
<li class="list-group-item">
<svg-icon icon-class="tree" />所属部门
<div class="pull-right" v-if="user.dept">{{ user.dept.deptName }} / {{ postGroup }}</div>
<div class="pull-right" v-if="user.dept">{{ user.dept.name }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="tree" />所属岗位
<div class="pull-right" v-if="user.posts">{{ user.posts.map(post => post.name).join(',') }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="peoples" />所属角色
<div class="pull-right">{{ roleGroup }}</div>
<div class="pull-right">{{ user.roles.map(post => post.name).join(',') }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="date" />创建日期
<div class="pull-right">{{ user.createTime }}</div>
<div class="pull-right">{{ parseTime(user.createTime) }}</div>
</li>
</ul>
</div>
@ -82,8 +86,6 @@ export default {
getUser() {
getUserProfile().then(response => {
this.user = response.data;
this.roleGroup = response.roleGroup;
this.postGroup = response.postGroup;
});
}
}

View File

@ -1,18 +1,18 @@
<template>
<el-form ref="form" :model="user" :rules="rules" label-width="80px">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="user.nickName" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="user.phonenumber" maxlength="11" />
<el-input v-model="user.nickname" />
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="user.mobile" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="user.email" maxlength="50" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="user.sex">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
@ -35,7 +35,7 @@ export default {
return {
//
rules: {
nickName: [
nickname: [
{ required: true, message: "用户昵称不能为空", trigger: "blur" }
],
email: [
@ -46,7 +46,7 @@ export default {
trigger: ["blur", "change"]
}
],
phonenumber: [
mobile: [
{ required: true, message: "手机号码不能为空", trigger: "blur" },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,

View File

@ -1,10 +1,21 @@
<template>
<div v-loading="loading" :style="'height:'+ height">
<iframe :src="src" frameborder="no" style="width: 100%;height: 100%" scrolling="auto" />
<div class="app-container">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportHtml">导出 HTML</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportWord">导出 Word</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportMarkdown">导出 Markdown</el-button>
</el-col>
</el-row>
<!-- 展示文档 -->
<div v-loading="loading" :style="'height:'+ height">
<iframe :src="src" frameborder="no" style="width: 100%;height: 100%" scrolling="auto" />
</div>
</div>
</template>
<script>
import {exportHtml} from "@/api/infra/dbDoc";
import { exportHtml, exportWord, exportMarkdown} from "@/api/infra/dbDoc";
export default {
name: "DBDoc",
@ -25,10 +36,31 @@ export default {
};
},
created() {
// Html
exportHtml().then(response => {
// var blob = new Blob(['<a id="a"><b id="b">hey!</b></a>'], {type : 'text/html'});
this.src = window.URL.createObjectURL(response);
let blob = new Blob([response], {type : 'text/html'});
this.src = window.URL.createObjectURL(blob);
})
},
methods: {
/** 处理导出 HTML */
handleExportHtml() {
exportHtml().then(response => {
this.downloadHtml(response, '数据库文档.html');
})
},
/** 处理导出 Word */
handleExportWord() {
exportWord().then(response => {
this.downloadWord(response, '数据库文档.doc');
})
},
/** 处理导出 Markdown */
handleExportMarkdown() {
exportMarkdown().then(response => {
this.downloadMarkdown(response, '数据库文档.md');
})
}
}
};
</script>

View File

@ -178,3 +178,11 @@ CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIG
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
commit;
-- 初始化默认任务 用户 Session 超时 Job
INSERT INTO QRTZ_JOB_DETAILS VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', NULL, 'cn.iocoder.dashboard.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000047400104A4F425F48414E444C45525F4E414D457400187379735573657253657373696F6E54696D656F75744A6F627800);
commit;
INSERT INTO QRTZ_TRIGGERS VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', 'sysUserSessionTimeoutJob', 'DEFAULT', NULL, 1615706340000, 1615706280000, 5, 'WAITING', 'CRON', 1615706125000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000007D074000F4A4F425F52455452595F434F554E547371007E0009000000037800);
commit;
INSERT INTO QRTZ_CRON_TRIGGERS VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', '0 * * * * ? *', 'Asia/Shanghai');
commit;

File diff suppressed because one or more lines are too long

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

@ -47,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);
}

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

@ -1,12 +1,38 @@
package cn.iocoder.dashboard.framework.datasource.config;
import cn.iocoder.dashboard.framework.datasource.core.filter.DruidAdRemoveFilter;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 数据库匹配类
* 数据库配置类
*
* @author 芋道源码
*/
@Configuration
@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理
public class DataSourceConfiguration {
/**
* 创建 DruidAdRemoveFilter 过滤器过滤 common.js 的广告
*/
@Bean
@ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true")
public FilterRegistrationBean<DruidAdRemoveFilter> druidAdRemoveFilterFilter(DruidStatProperties properties) {
// 获取 druid web 监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取 common.js 的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
// 创建 DruidAdRemoveFilter Bean
FilterRegistrationBean<DruidAdRemoveFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DruidAdRemoveFilter());
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}

View File

@ -0,0 +1,22 @@
package cn.iocoder.dashboard.framework.datasource.core.enums;
/**
* 对应于多数据源中不同数据源配置
*
* 通过在方法上使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解设置使用的数据源
* 注意默认是 {@link #MASTER} 数据源
*
* 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html
*/
public interface DataSourceEnum {
/**
* 主库推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解
*/
String MASTER = "master";
/**
* 从库推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解
*/
String SLAVE = "slave";
}

View File

@ -0,0 +1,38 @@
package cn.iocoder.dashboard.framework.datasource.core.filter;
import com.alibaba.druid.util.Utils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Druid 底部广告过滤器
*
* @author 芋道源码
*/
public class DruidAdRemoveFilter extends OncePerRequestFilter {
/**
* common.js 的路径
*/
private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
chain.doFilter(request, response);
// 重置缓冲区响应头不会被重置
response.resetBuffer();
// 获取 common.js
String text = Utils.readFromResource(COMMON_JS_ILE_PATH);
// 正则替换 banner, 除去底部的广告信息
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
text = text.replaceAll("powered.*?shrek.wang</a>", "");
response.getWriter().write(text);
}
}

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

@ -57,7 +57,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
}
// 当前登录用户不为空更新人为空则当前登录用户为更新人
if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) {
setFieldValByName("updater", loginUser.getId(), metaObject);
setFieldValByName("updater", loginUser.getId().toString(), metaObject);
}
}
}

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

@ -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(webProperties.getApiPrefix() + "/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

@ -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但是有统一的 codemessagerequestId 属性所以只好反射
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;
/**
* 短信长度例如说 123
*
* 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;
}

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