云E办后端
项目找maven
找到每个项目文件夹里的pom.xml右键选择 +add as maven project
邮箱密码:
zvpetxrypnaogiab
项目搭建
第一步:
创建一个springboot项目,将原有的src及其他目录删除,重新构建一个模块(maven modules,
).
父模块pom.xml增加一个packing。删除原有的构建和依赖。
父模块pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wyc</groupId>
<artifactId>my-endyeb</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>my-endyeb</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
</project>
第二步:
子模块选择quelistart,删除原有的bulid,和依赖,查看子工程有没有父工程坐标,没有就加入父工程项目坐标,让父子工程关联起来,子模块导入相关依赖,web,mysql,mybatis,lombok.将properties里面maven改为1.8,
子模块y-servicepom.xml
父工程坐标
<parent>
<artifactId>yeb</artifactId>
<groupId>com.wyc</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatisplus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
<!-- mybatisplus代码生成器依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- freemarker依赖,模板引擎-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<!-- swagger2 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- Swagger第三方ui依赖 因为swagger2原生的ui有点丑,所以就用了第三方的一个ui-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<!-- swaggerUI界面切换第一步:添加 原生 swaggerUI 依赖 -->
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<!-- <dependency>-->
<!-- <groupId>io.springfox</groupId>-->
<!-- <artifactId>springfox-swagger-ui</artifactId>-->
<!-- <version>2.7.0</version>-->
<!-- </dependency>-->
<!--Spring Security 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT 依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- google kaptcha依赖 -->
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- spring data redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--easy poi依赖-->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.1.3</version>
</dependency>
<!--rabbitmq 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--websocket 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 解决依赖导入不成功的问题:http://www.qishunwang.net/knowledge_show_129043.aspx -->
<!--FastDFS依赖,用于头像的更新-->
<dependency>
<groupId>org.csource</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.29-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.6.3</version>
</dependency>
<!-- 该依赖是针对日志的相关包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
</dependencies>
第三步
将java和test文件里面的原有文件删掉,创建项目相关的包。配置启动类。在main下创建resource包,在resource报下创建config包,在config里面创建application.yaml配置文件。将所有可以改变的值进行相应的改变。
application.yaml配置文件内容
server:
#端口
port: 8081 //改变
spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
thymeleaf:
cache: false #清除缓存
prefix: classpath:/templates/
# mvc:
# static-path-pattern: /admin/**
txcos:
secretId: AKIDiEVYP1RZ1GDwTiv5tSnt2yrXx97rrkvs
secretKey: wf9OPAnDUXieNq4xc5LTM3Xq05yd9QUN
bucket: ap-shanghai
bucketName: school-1251531734
path: https://school-1251531734.cos.ap-shanghai.myqcloud.com
prefix: exam
servlet:
multipart:
max-file-size: 5MB # 单个文件的最大值
max-request-size: 5MB # 上传文件总的最大值
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root //改变
password: 123456 //改变
url: jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai //改变
hikari:
#连接池名
pool-name: DateHikariCP
#最小空闲连接数
minimum-idle: 5
#空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
#最大连接数,默认10
maximum-pool-size: 10
#从连接池返回的连接的自动提交
auto-commit: true
#连接最大存活时间,0表示永久存活,默认1800000(30分钟)
max-lifetime: 1800000
#连接超时时间,默认3000(30秒)
connection-timeout: 30000
#测试是否可用的查询语句
connection-test-query: SELECT 1
main:
allow-circular-references: true
# redis:
# #数据库索引
# database: 0
# host: 127.0.0.1 //改变
# port: 6379
# password: //改变
# jedis:
# pool:
# #最大连接数
# max-active: 8
# #最大阻塞等待时间(负数表示没限制)
# max-wait: -1
# #最大空闲
# max-idle: 8
# #最小空闲
# min-idle: 0
# #连接超时时间
# timeout: 10000
# rabbitmq:
# addresses: 127.0.0.1 //改变
# port: 5672
# username: guest //改变
# password: guest //改变
# virtual-host: /
# JWT配置
## JWT配置 //注解要是拿不到,就主要看配置文件命名是否一致
jwt:
# JWT存储的请求头
tokenHeader: Authorization //改变
# JWT 加解密使用的密钥
secret: yeb-secret
# JWT的超期限时间(60*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer //改变
#Mybatis-plus配置
mybatis-plus:
mapper-locations: classpath*:/mapper/*Mapper.xml
configuration:
#自动驼峰命名
map-underscore-to-camel-case: false
## mybatis sql 打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
level:
com.wyc.mbg.mapper: debug //改变
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
spring:
main:
web-application-type: none
到此,项目构建基本完成。
项目启动
将test文件和java文件下的app文件删掉,编写主启动类。
编写主启动类
package com.wyc;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author superBrother
* @create 2022/7/2
* @Description
*/
@SpringBootApplication
@MapperScan("com.wyc.mbg.mapper")
@EnableScheduling
//// swaggerUI界面切换第三步:在启动类加上 @EnableSwagger2 注解(前面两步修改后,能成功访问,这步就不需要做了)
@EnableSwagger2
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
使用代码生成器完成重复性的单表操作:可以直接在test目录下创建CodeGenerator类,进行代码自动生成.
建类CodeGenerator
CodeGenerator代码:将注释里面有改变的进行改变为自己项目相对应的内容。
package com.wyc;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @author superBrother //改变
* @create 2021/11/20
*/
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
//需要修改模块的路径 yeb-server
gc.setOutputDir(projectPath + "/yeb-server/src/main/java"); //改变
//作者
gc.setAuthor("superBrother");
//打开输出目录
gc.setOpen(false);
//xml开启 BaseResultMap
gc.setBaseResultMap(true);
//xml 开启BaseColumnList
gc.setBaseColumnList(true);
// 实体属性 Swagger2 注解
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia" +
"/Shanghai"); //改变
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root"); //改变
dsc.setPassword("123456"); //改变
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.wyc.mbg") //改变
.setEntity("pojo")
.setMapper("mapper")
.setService("service")
.setServiceImpl("service.impl")
.setController("controller");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
//需要修改模块的路径 yeb-server //改变
return projectPath + "/yeb-server/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper"
+ StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
//数据库表映射到实体的命名策略
strategy.setNaming(NamingStrategy.underline_to_camel);
//数据库表字段映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.no_change);
//lombok模型
strategy.setEntityLombokModel(true);
//生成 @RestController 控制器
strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
//生成表时,去掉表前缀
strategy.setTablePrefix("t_"); //没有注释掉 改变
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- swagger第三方ui依赖-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
登录
jwt token 工具类编写
第一步:先导入security依赖和jwt依赖
<!--Spring Security 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT 依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
第二步:在application.yml进行jwt的配置
## JWT配置
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: yeb-secret
# JWT的超期限时间(60*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer
第三步:创建config包,然后创建security包,创建JwtTokenUtil实例类,编写jwtToken。
package com.xxxx.server.config.security.component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
/**
* Created by HMF on 2021/07/23 11:48
*/
@Component // @Component注解使用在任意层中,交给IOC实例化
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME = "sub"; // 用户名的key
private static final String CLAIM_KEY_CREATED = "created"; // Jwt创建时间的key
// 从配置文件application.yml中拿到JWT的配置内容,使用@Value注解绑定
@Value("${jwt.secret}")
private String secret; // JWT加解密使用的密钥
@Value("${jwt.expiration}")
private Long expiration; // JWT的超期限时间(60*60*24)
/**
* 【根据用户信息生成token:】
* 用户信息根据Spring Security的UserDetails来获取 ////////////// 实际可以被调用的方法
*
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// 自定义申明的键值对
// 主体,用户 {"sub": "Rose"},如 .setSubject("Rose")
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
// 创建日期 {"iat": "当前的时间"},如 .setIssuedAt(new Date())
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 【从Token中获取登录用户名】 ///////// 实际可以被调用的方法
*
* @param token
* @return
*/
public String getUserNameFromToken(String token) {
// 先去从token中获取荷载,因为用户名放在了荷载里面
String username;
try {
Claims claims = getClaimsFromToken(token); // 获取荷载,从token中,即解析token,通过 claims.getSubject() 获取用户名
username = claims.getSubject();
} catch (Exception e) {
// 有异常时,用户名重新赋值 null
username = null;
e.printStackTrace();
}
return username;
}
/**
* 【验证token是否有效:(需要判断两个方面)】 ///////// 实际可以被调用的方法
* 1.判断token是否过期
* 2.判断token荷载里面的用户名和UserDetails里的用户名是否一致
*
* @param token
* @param userDetails
* @return
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 【判断token是否可以被刷新】 ///////// 实际可以被调用的方法
*
* @param token
* @return
*/
public boolean canRefresh(String token) {
// 如果过期就可以被刷新
return !isTokenExpired(token);
}
/**
* 【刷新token】 ///////// 实际可以被调用的方法
*
* @param token
* @return
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
// 重新设置token的创建时间为当前时间,从而重新生成token,达到刷新token的目的
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
//////////////////////////////// 以下都是一些内部的私有方法 ////////////////////////////////
/**
* 判断token是否失效过期
*
* @param token
* @return
*/
private boolean isTokenExpired(String token) {
Date expiredData = getExpiredDateFromToken(token);
// 直接使用 before() 方法,判断失效时间是否在当前时间的前面
return expiredData.before(new Date());
}
/**
* 从token中获取过期时间
*
* @param token
* @return
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token); // 从token中获取荷载
return claims.getExpiration(); // 从荷载中获取过期时间,然后return
}
/**
* 从token中获取荷载,即 解析传入的 token
*
* @param token
* @return
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
// 解析 token,获取负载中声明的对象
claims = Jwts.parser()
// 解析的秘钥 secret 即 yeb-secret
.setSigningKey(secret)
// 需要被解析的token
.parseClaimsJws(token)
// 解析之后的主体
.getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 方法的重载:
* 根据荷载构建生成JWT TOKEN
*
* @param claims
* @return
*/
private String generateToken(Map<String, Object> claims) { // 方法的重载
return Jwts.builder()
// 自定义申明,键值对的形式
.setClaims(claims)
// 设置 token 的失效时间是 24h
.setExpiration(generateExpirationDate())
// 签名算法 HS512 和 secret 盐 yeb-secret(在配置文件中配置了内容)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 生成token失效时间
*
* @return
*/
private Date generateExpirationDate() {
// 设置为 当前时间 + 设置的过期限时间 (24小时)
return new Date(System.currentTimeMillis() + expiration);
}
}
第四步:编写公共返回对象,在wyc/common包下建立RespBean,进行编写RespBean,还可以自定义其他类的编写。
package com.xxxx.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 公共返回对象
* Created by HMF on 2021/07/23 15:07
*/
@Data // getter和setter方法
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 全参构造
public class RespBean { ///////// 公共返回对象 //////////
private long code; // 状态码
private String message; // 提示消息
private Object obj; // 返回的对象
/**
* 成功返回结果
*
* @param message
* @return
*/
public static RespBean success(String message) {
return new RespBean(200, message, null);
}
/**
* 成功返回结果
*
* @param message
* @param obj
* @return
*/
public static RespBean success(String message, Object obj) {
return new RespBean(200, message, obj);
}
/**
* 失败返回结果
*
* @param message
* @return
*/
public static RespBean error(String message) {
return new RespBean(500, message, null);
}
/**
* 失败返回结果
*
* @param message
* @param obj
* @return
*/
public static RespBean error(String message, Object obj) {
return new RespBean(500, message, obj);
}
}
pageR
package com.wyc.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author superBrother
* @create 2022/7/2
* @Description
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageR {
/**
* 每页显示大小
*/
private long size;
/**
* 当前页码
*/
private long current;
/**
* 最大页数
*/
private long maxCurrent;
/**
* 数据总条数
*/
private long total;
}
R
package com.wyc.common;
import java.util.List;
/**
*
* @author superBrother
* @date 2020/7/2
* @ Description:
*/
public class R {
//状态,成功:1,失败:0
private int status;
//消息
private String msg;
//数据
private Object data;
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public R() {
}
public R(int status, String msg) {
this.status = status;
this.msg = msg;
this.data = null;
}
public R(int status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public R(ResultStatus resultStatus) {
this.status = resultStatus.getStatus();
this.msg = resultStatus.getMsg();
this.data = null;
}
public R(ResultStatus resultStatus, Object data) {
this.status = resultStatus.getStatus();
this.msg = resultStatus.getMsg();
this.data = data;
}
public R(ResultStatus resultStatus, String str) {
this.status = resultStatus.getStatus();
this.msg = resultStatus.getMsg() + ":[" + str +"]";
}
//具体成功信息,带数据(2个参数)
public static R ok(ResultStatus ok, Object data) {
//查询集合为空的统一提示处理
if(data instanceof List){
List list = (List)data;
}
return new R(ok, data);
}
//成功信息,不带数据。
public static R ok() {
return new R(ResultStatus.SUCCESS);
}
//具体成功信息,不带数据。
public static R ok(ResultStatus ok) {
return new R(ok);
}
//具体错误信息。
public static R error(ResultStatus error) {
return new R(error);
}
//具体错误信息(加附带信息)。
public static R error(ResultStatus error, String str) {
return new R(error,str);
}
/** 格式化占位符*/
public static R format(ResultStatus rs, String ...value){
StringBuffer sbMsg = new StringBuffer(rs.getMsg());
int index = 0;
for(int i=0,l=value.length;i<l;i++){
String old = "{"+i+"}";
index = sbMsg.indexOf(old,index);
sbMsg = sbMsg.replace(index,index+old.length(),value[i]);
}
return new R(rs.getStatus(),sbMsg.toString());
}
/** 首部添加信息*/
public static R appendBegin(R r, String addMsg){
r.setMsg(addMsg+","+r.getMsg());
return r;
}
/** 尾部添加信息*/
public static R appendEnd(R r, String addMsg){
r.setMsg(r.getMsg()+","+addMsg);
return r;
}
@Override
public String toString() {
return "Result{" +
"status=" + status +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
ResultStatus
package com.wyc.common;
/**
* @author superBrother
* @create 2022/7/2
* @Description
*/
public enum ResultStatus {
/**
* 500:失败
* 200:成功
*/
/**
* info
*/
NOT_LOGIN(401,"未登录,请登录"),
NOT_PERMISSION(403,"权限不足,请联系管理员"),
NOT_ID(500,"未传入id"),
ID_NOT_EXIST(500,"该id不存在"),
MIN_NOT_GREATER_MAX(500,"最小值不能大于最大值"),
NEED_ID(500,"请传入id"),
USER_EXIST(500,"该用户已经存在"),
USER_NOT_EXIST(500,"该用户不存在"),
USER_IS_FORBIDDEN(500,"该用户已经被禁用了"),
MOBILE_EXIST(500,"该电话已经存在"),
EMAIL_EXIST(500,"该邮箱已经存在"),
TEACHER_NOT_EXIST(500,"该用户不存在"),
COURSE_NOT_EXIST(500,"该课程不存在"),
SEMESTER_NOT_EXIST(500,"该学期不存在"),
CLAZZ_ID_NOT_EXIST(500,"传入的班级id不存在"),
FILE_IS_NULL(500,"传入的文件为空"),
UPLOAD_ERROR(500,"上传失败"),
/**
* 成功信息
*/
SUCCESS(200, "成功"),
ADD_SUCCESS(200, "增加成功"),
DELETE_SUCCESS(200, "删除成功"),
UPDATE_SUCCESS(200, "更改成功"),
SELECT_SUCCESS(200, "查询成功"),
SINGUP_SUCCESS(200, "注册成功"),
LOGIN_SUCCESS(200, "登录成功"),
LOGOUT_SUCCESS(200, "退出成功"),
GAIN_SUCCESS(200,"获取成功"),
EMAIL_SEND_SUCCESS(200,"邮箱发送成功"),
UPLOAD_SUCCESS(200,"上传文件成功"),
/**
* 失败
*/
ERROR(500,"失败"),
ADD_ERROR(500,"添加失败"),
UPDATE_ERROR(500,"修改失败"),
DELETE_ERROR(500,"删除失败"),
SELECT_ERROR(500,"查询失败"),
LOGIN_ERROR(500,"登录失败"),
TYPE_ERROR(500,"类型转换错误"),
ASSOCIATED_DATA_ERROR(500,"该数据有关联数据,操作失败!"),
MYSQL_ERROR(500,"数据库异常,操作失败!"),
BUSINESS_ERROR(500,"业务出现错误,请联系后端开发人员"),
PASSWORD_ERROR(500,"密码错误,请重新输入"),
EMAIL_FORMAT_ERROR(500,"邮箱格式错误"),
EMAIL_SEND_ERROR(500,"邮箱发送失败"),
ID_IS_NOT_TEACHER_ID(500,"传入的id不是老师id"),
;
/**
* 返回码
*/
private int status;
/**
* 返回结果描述
*/
private String msg;
ResultStatus() {
}
ResultStatus(int status, String msg) {
this.status = status;
this.msg = msg;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
登录流程:
前端输入用户密码传给后端,后端判断用户名密码是否正确,如果用户名密码正确,就会生成一个jwt令牌,返回给前端。如果用户名和密码不正确,我们就让他重新输入。
前端拿到生成的jwt令牌之后,他就会把它放在请求头里面,后面每一次请求都会携带这个jwt令牌,后端写一个jwt令牌相关的拦截器,每次请求之前都走一下拦截器,判断一下jwt令牌是否存在,以及令牌是否正确,合法有效。如果不存在,或者不是合法有效的jwt令牌的话,就拦截掉了,不让他去访问我们其他的接口,如果它的jwt令牌判断出来是合法有效的,我们就允许访问我们其他的接口。
第五步:在Admin类中实现UserDetails.
Admin.java
package com.xxxx.server.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.xxxx.server.config.CustomAuthorityDeserializer;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
*
* </p>
*
* @author HMF
* @since 2021-07-23
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_admin")
@ApiModel(value = "Admin对象", description = "")
public class Admin implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "姓名")
private String name;
@ApiModelProperty(value = "手机号码")
private String phone;
@ApiModelProperty(value = "住宅电话")
private String telephone;
@ApiModelProperty(value = "联系地址")
private String address;
@ApiModelProperty(value = "是否启用")
@Getter(AccessLevel.NONE)
private Boolean enabled;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "用户头像")
private String userFace;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "角色权限")
@TableField(exist = false)
private List<Role> roles;
///////////// 实现 UserDetails 接口需要重写的方法 /////////////////////
@Override
@JsonDeserialize(using = CustomAuthorityDeserializer.class) /// 使用自定义的反序列化
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = roles
.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
实现之后,将对应方法的返回值全部改为true.
创建param包,然后创建AdminLoginParams类,专门用来接受前端传过来的参数。(用户登录实体类)
AdminLoginParams.java
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户登录实体类:
* (其实就是登录所需的参数不需要那么多,又另外搞个实体类)
* Created by HMF on 2021/07/23 15:31
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "AdminLogin对象", description = "")
public class AdminLoginParam {
@ApiModelProperty(value = "用户名", required = true)
private String username;
@ApiModelProperty(value = "密码", required = true)
private String password;
@ApiModelProperty(value = "验证码", required = true)
private String code;
}
在controller包下创建LoginController
1.首先注入service,返回公用的返回对象,RespBean。编写方法。
@Autowired
private IAdminService adminService; // 注入service
/**
* 登录之后返回token
*
* @param adminLoginParam
* @param request
* @return
*/
@ApiOperation(value = "登录之后返回token")
@PostMapping("/login")
public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request) {
return adminService.login(adminLoginParam.getUsername(), adminLoginParam.getPassword(), adminLoginParam.getCode(), request);
}
2.编写service。
spring-security主要通过UserDetails中的loadUserByUsername来实现的。获取到UserDetails.
,所以先注入UserDetailsService.
密码:前端传的是明文密码,但数据库存的肯定是加密的密码,而Security提供了一个专门加密密码的工具,它就是PasswordEncoder.通过PasswordEncoder进行匹配,它第一个参数是我们前端传的密码,第二个是UserDetails自己获取的密码。
获取到UserDetails先进行判断一下,如果为空,或者密码不正确,就返回用户名和密码不正确。
再加一个判断,如何用户被禁用,提示联系管理员。
登录成功后,我们使用spring-security框架,在去获取相应的用户信息的时候,可以用spring-security提供的对象,如UserDetails,获取对象,就要把对象放在spring-security的全局中,下次对象就是我们登录的。
否则就登录成功。登录成功,根据UserDetails拿到令牌token。注入jwtTokenUtil.
通过JwtTokenUtil拿到token,创建tokenmap,将token和tokenHead添加到tokemap。
将tokenmap返回给前端,并给出提示信息。
// 获取验证码,我们把生成的验证码存在了session中,直接获取就可以了
String captcha = (String) request.getSession().getAttribute("captcha");
System.out.println("================");
System.out.println(captcha);
// 先判断用户是否输入了验证码,然后判断前端输入的验证码与生成的存在session中的验证码是否一致
if (StringUtils.isEmpty(code) || !captcha.equalsIgnoreCase(code)) {
// 验证码错误时,直接返回携带错误消息
return RespBean.error("验证码输入错误,请重新输入!");
}
// 登录
// 通过Spring Security里面的UserDetailsService的loadUserByUsername()方法实现登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
return RespBean.error("用户名或密码不正确!");
}
if (!userDetails.isEnabled()) {
return RespBean.error("账号被禁用,请联系管理员!");
}
// 更新security登录用户对象
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
//【把登录的用户对象放到SpringSecurity全局中】
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 如果前面的判断都通过了,就拿到一个令牌 token
String token = jwtTokenUtil.generateToken(userDetails);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead); // 这个是给前端,让她们放到请求头中的
return RespBean.success("登录成功", tokenMap);
}
退出登录:
正常的流程是登录之后,返回token,前端会把token放在请求头里面,访问任何接口,都会携带token令牌,我们的拦截器会判断这个令牌是否合法有效,如果合法有效,才能访问其他的接口,如果是非法的,就不让访问。我们和前端定义好,让前端直接调用这个退出登录的接口,我们后端给他返回状态码200,拿到这个状态码200之后,前端直接在请求头里面将token删除,这是用户在请求页面,调后端接口,就会被我们的拦截器拦截,不让访问,这样我们就完成了退出登录的处理,后端就不需要在做额外的处理,同样我们后端也可以直接做处理,这就需要前后端一起商量了。
/**
* 退出登录
*
* @return
*/
@ApiOperation(value = "退出登录")
@PostMapping("/logout")
public RespBean logout() {
return RespBean.success("注销成功!");
}
}
获取当前用户信息 controller
/**
* 获取当前登录用户的信息
*
* @param principal
* @return
*/
@ApiOperation(value = "获取当前登录用户的信息")
@GetMapping("/admin/info")
//在运行过程中,Spring 会将 Username、Password、Authentication、Token 注入到 Principal 接口中,我们可以直接获取使用
public Admin getAdminInfo(Principal principal) { // 因为我们把当前登录对象设置到了Spring Security的全局中,所以这里可以通过Principal直接获取
if (null == principal) {
return null;
}
String username = principal.getName();
// 通过用户名username获取一个完整的用户对象
Admin admin = adminService.getAdminByUserName(username);
admin.setPassword(null); // 因为用户密码不能被返回,所以要设置为null
admin.setRoles(adminService.getRoles(admin.getId()));
return admin;
}
service
/**
* 根据用户名username获取完整的用户对象 接口方法
*
* @param username
* @return
*/
Admin getAdminByUserName(String username);
impl
/**
* 根据用户名username获取完整的用户对象 实现方法
*
* @param username
* @return
*/
@Override
public Admin getAdminByUserName(String username) {
// 因为使用了MyBatisPlus,所以查询数据就变的更简单了
//正常的话还有判断这个adminMapper是否为空,健壮性。
return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username", username).eq("enabled", true)); // 可以使用链式编程,一直...eq()方法
}
配置spring-security。
在config/security新建SecurityConfig配置类
import com.wyc.mbg.pojo.Admin;
import com.wyc.mbg.service.IAdminRoleService;
import com.wyc.mbg.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author:superBrother
* @create: 2022-07-02 19:16
* @Description: Security配置类
*/
@Configuration // 配置类需要加的注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IAdminService adminService;
@Autowired
private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
// @Autowired
// private CustomFilter customFilter;
//
// @Autowired
// private CustomUrlDecisionManager customUrlDecisionManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
"/index.html",
"favicon.ico",
"/doc.html",
// "/swagger-ui.html", // swaggerUI界面切换第二步:放行 原生swaggerUI的路径访问
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/captcha", // 验证码获取放行
"/ws/**" // websocket聊天放行,注意别忘了 /
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 使用JWT,不需要csrf
http.csrf().disable()
// 基于token, 不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 所有请求都要求认证
.anyRequest()
.authenticated()
// 动态权限配置
// .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
// @Override
// public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// object.setAccessDecisionManager(customUrlDecisionManager);
// object.setSecurityMetadataSource(customFilter);
// return object;
// }
// })
.and()
// 禁用缓存
.headers()
.cacheControl();
// 添加jwt,登录授权过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
// 添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
@Override
@Bean // @Bean注解,返回的对象交给IOC容器管理
public UserDetailsService userDetailsService() {
// 通过箭头函数重写 UserDetailsService 下的 loadUserByUsername()方法
return username -> { // 箭头代表将前面的参数username传递给后面的代码
Admin admin = adminService.getAdminByUserName(username);
if (null != admin) {
// admin.setRoles(adminService.getRoles(admin.getId()));
return admin;
}
// 抛出SpringSecurity提供的异常 UsernameNotFoundException
throw new UsernameNotFoundException("用户名或密码不正确!");
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
}
Jwt登录过滤器 JwtAuthencationTokenFilter
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;
/**
* JWT登录授权过滤器
* <p>
* Created by HMF on 2021/07/23 17:24
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(tokenHeader);
// 存在token
if (null != authHeader && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
// token存在用户名但未登录
if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
// 登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 验证token是否有效,重新设置用户对象
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 重新设置到用户对象
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
当未登录或者token失效时访问接口时,自定义的返回结果
RestAuthorizationEntryPoint.java
/**
* 当未登录或者token失效时访问接口时,自定义的返回结果
* Created by HMF on 2021/07/23 17:53
*/
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
RespBean bean = RespBean.error("尚未登录,请登录!");
bean.setCode(401);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
当访问接口没有权限时,自定义返回结果
/**
* 当访问接口没有权限时,自定义返回结果
* Created by HMF on 2021/07/23 18:01
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
RespBean bean = RespBean.error("权限不足,请联系管理员!");
bean.setCode(403);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
swagger配置 导入相关依赖,将改变的地方改变。
package com.wyc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.service.*;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
/**
* @author:superBrother
* @create: 2022-07-02 20:08
* @Description: Swagger2配置类
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.wyc.mbg.controller")) //改变
.paths(PathSelectors.any())
.build()
//全局访问认证
.securityContexts(securityContexts())
.securitySchemes(securitySchemes());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("云E办接口文档")
.description("云E办接口文档")
.contact(new Contact("superBrother", "https://localhost:8081", "superbroo@qq.com")) //改变
.version("1.0")
.build();
}
private List<ApiKey> securitySchemes() {
// 设置请求头信息
List<ApiKey> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("Authorization", "Authorization", "Header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts() {
// 设置需要登录的认证路径
List<SecurityContext> result = new ArrayList<>();
result.add(getContextByPath("/hello/.*"));
return result;
}
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
}
在controller编写测试接口
package com.xxxx.server.controller;
/**
* Created by HMF on 2021/07/23 20:37
*/
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试接口用的controller
*/
@RestController
public class HelloController {
@GetMapping("hello")
public String hello() {
return "hello!!!";
}
@GetMapping("/employee/basic/hello")
public String hello2() {
return "/employee/basic/hello";
}
@GetMapping("/employee/advanced/hello")
public String hello3() {
return "/employee/advanced/hello";
}
}
启动发现被拦截,修改Security配置,放行这个接口。 SecurityConfig
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
"/index.html",
"favicon.ico",
"/doc.html",
//"/swagger-ui.html", // swaggerUI界面切换第二步:放行 原生swaggerUI的路径访问
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/captcha", // 验证码获取放行
"/ws/**" // websocket聊天放行,注意别忘了 /
);
}
登录验证码
使用谷歌的验证码
<!-- google kaptcha依赖 -->
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
编写验证码配置类
CaptchaConfig
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* 谷歌图形验证码配置类
* <p>
* Created by HMF on 2021/07/24 21:44
*/
@Configuration // 配置了的@Configuration注解不要忘记了
public class CaptchaConfig {
/**
* 配置直接copy文档的
* DefaultKaptcha 验证码生成器
*
* @return
*/
@Bean // @Bean注解,返回的对象交给IOC处理
public DefaultKaptcha defaultKaptcha() {
// 验证码生成器
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
// 配置
Properties properties = new Properties();
//是否有边框
properties.setProperty("kaptcha.border", "yes");
// 设置边框颜色
properties.setProperty("kaptcha.border.color", "105,179,90");
// 边框粗细度,默认为1
// properties.setProperty("kaptcha.border.thickness","1");
// 验证码
properties.setProperty("kaptcha.session.key", "code");
// 验证码文本字符颜色 默认为黑色
properties.setProperty("kaptcha.textproducer.font.color", "blue"); //改变
// 设置字体样式
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅 黑");
// 字体大小,默认40
properties.setProperty("kaptcha.textproducer.font.size", "30");
// 验证码文本字符内容范围 默认为abced2345678gfynmnpwx
// properties.setProperty("kaptcha.textproducer.char.string", "");
// 字符长度,默认为5
properties.setProperty("kaptcha.textproducer.char.length", "4");
// 字符间距 默认为2
properties.setProperty("kaptcha.textproducer.char.space", "4");
//验证码图片宽度 默认为200
properties.setProperty("kaptcha.image.width", "100");
// 验证码图片高度 默认为40
properties.setProperty("kaptcha.image.height", "40");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
验证码接口 controller
package com.xxxx.server.controller;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
/**
* 谷歌验证码接口Controller
* <p>
* Created by HMF on 2021/07/24 21:51
*/
@RestController
public class CaptchaController {
@Autowired
private DefaultKaptcha defaultKaptcha;
@ApiOperation(value = "验证码")
@GetMapping(value = "/captcha", produces = "image/jpeg") //解决乱码
public void captcha(HttpServletRequest request, HttpServletResponse response) {
// 返回的是一个图形验证码,通过流的方式直接传一个正常的图片过去
// 那么响应头需要做一些处理,这里处理基本上来说固定的
// 定义response输出类型为image/jpeg类型
response.setDateHeader("Expires", 0);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
// return a jpeg
response.setContentType("image/jpeg");
//-------------------生成验证码 begin --------------------------
// 获取验证码文本内容
String text = defaultKaptcha.createText();
System.out.println("验证码内容:" + text);
// 将验证码放入session中
request.getSession().setAttribute("captcha", text);
// 根据文本内容创建图形验证码
BufferedImage image = defaultKaptcha.createImage(text);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
// 输出流输出图片,格式jpg
ImageIO.write(image, "jpg", outputStream);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//-------------------生成验证码 end ----------------------------
}
}
并在SecurityConfig放行验证码接口。
token过期:JWT expired at 2022-07-04T07:40:55Z. Current time: 2022-07-04T08:41:37Z, a difference of 3642448 milliseconds. Allowed clock skew: 0 milliseconds.
1.清空浏览器
2.查看其他方法
https://blog.51cto.com/u_15127672/4137173
修改AdminLoginParam类,加入验证码
package com.wyc.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @author:superBrother
* @create: 2022-07-02 15:02
* @Description: 用户登录实体类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "AdminLogin对象",description = "")
public class AdminLoginParam {
@ApiModelProperty(value = "用户名",required = true)
private String username;
@ApiModelProperty(value = "密码",required = true)
private String password;
@ApiModelProperty(value = "验证码",required = true)
private String code;
}
去登录接口修改(LoginController)
return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request); //添加验证码
service
/**
* 登录之后返回token
* @param username
* @param password
* @param code
* @param request
* @return
*/
RespBean login(String username, String password, String code, HttpServletRequest request);
impl
/**
* 登录之后返回token
* @param username
* @param password
* @param code
* @param request
* @return
*/
@Override
public RespBean login(String username, String password, String code, HttpServletRequest request) {
String captcha =(String) request.getSession().getAttribute("captcha"); //拿到验证码进行匹配
if (StringUtils.isEmpty(code)||!captcha.equalsIgnoreCase(code)){ //如果验证码错误,直接返回
return RespBean.error("验证码输入错误,请重新输入!");
}
到此,登录基本完成。
菜单功能
菜单可能会设计权限等等。
根据登录用户id查询对应菜单列表。
在pojo/menu中加入注解,代表数据库没有此字段,防止查询时出错。
@ApiModelProperty(value = "子菜单")
@TableField(exist = false) // 因为数据库表没有这个字段,所以让程序识别这个字段不在数据库表中
private List<Menu> children;
在controller层写接口
package com.xxxx.server.controller;
import com.xxxx.server.pojo.Menu;
import com.xxxx.server.service.IMenuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author HMF
* @since 2021-07-23
*/
@RestController
@RequestMapping("/system/config") /菜单做了url
public class MenuController {
@Autowired
private IMenuService menuService;
@ApiOperation(value = "通过用户id查询菜单列表")
@GetMapping("/menu")
public List<Menu> getMenusByAdminId() {
return menuService.getMenusByAdminId();
}
}
//为什么通过用户id查询菜单列表,controller也没有传用户id?
因为用户能正常登录之后,用户的相关信息都是后端直接获取的,而不需要前端传进来,因为前端传进来可能会出问题,可能或被别人篡改。
如何从后端获取?
SpringSecurity全局对象获取,只要登录了之后,就可以通过全局对象获取到用户id。
service
/**
* 通过用户id查询菜单列表 接口方法
*
* @return
*/
List<Menu> getMenusByAdminId();
impl
/**
* 通过用户id查询菜单列表 实现方法
*
* @return
*/
@Override
public List<Menu> getMenusByAdminId() {
// Integer adminId = ((Admin) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId();
// 后期通过封装AdminUtils实现
Integer adminId = AdminUtils.getCurrentAdmin().getId();
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
// 从redis获取菜单数据
List<Menu> menus = (List<Menu>) valueOperations.get("menu_" + adminId);
// 如果redis中为空,去数据库获取
if (CollectionUtils.isEmpty(menus)) { // 如果没有
menus = menuMapper.getMenusByAdminId(adminId);
// 将数据设置到redis中
valueOperations.set("menu_" + adminId, menus);
}
return menus;
}
mapper
/**
* 通过用户id查询菜单列表
*
* @param id
* @return
*/
List<Menu> getMenusByAdminId(Integer id);
mapper.xml
根据用户信息拿到菜单列表
思路:根据什么在什么表中拿到什么?
根据 xxx 在 xxx表xxx 中拿到 xxx
根据 用户id adminId 在 用户表admin_role 中拿到 用户权限rid,
根据 用户权限 rid 在 用户权限表menu_role 中拿到 用户菜单mid,
根据 用户菜单 MID 在 用户菜单表menu 中拿到 用户菜单信息
主菜单信息通过子菜单parentId来拿, m1.id = m2.parentId
子菜单通过菜单权限表的菜单rid来拿。 m2.id = mr.mid mr.rid = ar.rid
菜单rid通过用户id来拿 ar.adminId = 3
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.wyc.mbg.pojo.Menu">
<id column="id" property="id" />
<result column="url" property="url" />
<result column="path" property="path" />
<result column="component" property="component" />
<result column="name" property="name" />
<result column="iconCls" property="iconCls" />
<result column="keepAlive" property="keepAlive" />
<result column="requireAuth" property="requireAuth" />
<result column="parentId" property="parentId" />
<result column="enabled" property="enabled" />
</resultMap>
将主菜单通用的查询映射结果复制到子菜单,然后将column全部改为xx2.
<resultMap id="Menus" type="com.wyc.mbg.pojo.Menu" extends="BaseResultMap">
<collection property="children" ofType="com.wyc.mbg.pojo.Menu">
<id column="id2" property="id" />
<result column="url2" property="url" />
<result column="path2" property="path" />
<result column="component2" property="component" />
<result column="name2" property="name" />
<result column="iconCls2" property="iconCls" />
<result column="keepAlive2" property="keepAlive" />
<result column="requireAuth2" property="requireAuth" />
<result column="parentId2" property="parentId" />
<result column="enabled2" property="enabled" />
</collection>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, url, path, component, name, iconCls, keepAlive, requireAuth, parentId, enabled
</sql>
将原来的resultType="com.wyc.mbg.pojo.Menu"换为:resultMap="Menus",因为多了一个子菜单children。
<!-- 根据用户id查询菜单列表-->
<select id="getMenusByAdminId" resultMap="Menus">
SELECT DISTINCT
m1.*,
m2.id AS id2,
m2.url AS url2,
m2.path AS path2,
m2.component AS component2,
m2.`name` AS name2,
m2.iconCls AS iconCls2,
m2.keepAlive AS keepAlive2,
m2.requireAuth AS requireAuth2,
m2.parentId AS parentId2,
m2.enabled AS enabled2
FROM
t_menu m1,
t_menu m2,
t_admin_role ar,
t_menu_role mr
WHERE
m1.id = m2.parentId
AND m2.id = mr.mid
AND mr.rid = ar.rid
AND ar.adminId = #{id}
AND m2.enabled = TRUE
ORDER BY
m2.id
</select>
由于菜单不会有太大变化,而且需要频繁的渲染,那么就将这个菜单放到redis里面。
加入redis
导入redis相关依赖
<!-- spring data redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在db配置文件里面配置redis
redis:
# 超时时间
timeout: 10000ms
# 服务器地址(学习时用的是虚拟机的ip地址)
# host: 192.168.1.105
host: 127.0.0.1
# 服务器端口
port: 6379
# 选择的数据库
database: 1
# 密码(学习时,没有密码就不用配了)
# password: 123456
# 使用lettuce操作redis(还有Jedis也可以操作Redis)
lettuce:
pool:
# 最大连接数,默认是8
max-active: 1024
# 最大连接阻塞等待时间,默认-1
max-wait: 10000ms
# 最大空闲连接,默认是8
max-idle: 200
# 最小空闲连接,默认是0
min-idle: 5
编写redis配置类,序列化.
config/RedisConfig
package com.xxxx.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Created by HMF on 2021/07/28 21:52
* <p>
* Redis配置类,设置序列化
*/
@Configuration
public class RedisConfig {
/**
* 设置序列化
*
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// String类型 key序列器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// String类型 value序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// Hash类型 key序列器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Hash类型 value序列器
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
// 把连接工厂放进去
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
序列化完成之后,去修改MenusServiceImpl.
//首先注入redisTemplate
@Autowired
private RedisTemplate redisTemplate;
/**
* 通过用户id查询菜单列表
* @return
*/
@Override
public List<Menu> getMenusByAdminId() {
//通过全局对象,拿到adminId
Integer adminId =((Admin) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId();
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
//从redis获取菜单数据
List<Menu> menus =(List<Menu>) valueOperations.get("menu_" + adminId);
//如果为空,去数据库获取
if(CollectionUtils.isEmpty(menus)){
menus = menuMapper.getMenusByAdminId(adminId);
//将数据设置到Redis中
valueOperations.set("menu_"+adminId,menus);
}
return menus;
}
如果对数据库的菜单进行修改,一定要清空redis数据库,不然查到的全是之前的数据。
用户权限
加入用户权限,让用户在自己的权限范围内访问自己的接口。
权限管理RBAC基本概念。
RBAC是基于角色的访问控制(Role-Based Access Control)在RBAC中,权限与角色相关联,用户通过扮演适当的角色从而得到这些角色的权限。这样管理层都是层级相互依赖的,权限赋予给角色,角色又赋予给用户,这样的权限设计很清楚,管理起来也很方便。
RBAC授权实际上是who,what,how三元组之间的关系,也就是who对what进行how的操作,简单说就是谁对什么资源做了怎样的操作。
这里用户与角色实体对应的关系为多对多,角色与资源对应的关系同样为多对多关系,所以在实体设计上用户与角色间增加用户角色实体,将多对多的对应关系拆分为一对多,同理,角色与资源多对多对应关系拆分出中间实体对象权限实体。
基础的五表
进行角色判断流程
首先是处于登录的状态之后,做相应的前提判断。没登录就没有所谓的角色,也就不需要判断。
1.根据登录之后,判断自己的角色。
2.根据请求的url,判断角色。假设我们访问一个url,我们会个根据url找到这个菜单id,根据mid在菜单权限表中判断这个id是否拥有访问这个url的权限。
根据访问url判断角色
修改pojo/menu
加入角色列表
@ApiModelProperty(value = "角色列表")
@TableField(exist = false)
private List<Role> roles;
修改menuService
/**
* 根据角色获取菜单列表
* @return
*/
List<Menu> getMenusWithRole();
修改menuServiceImpl
/**
* 根据角色获取菜单列表
* @return
*/
@Override
public List<Menu> getMenusWithRole() {
return menuMapper.getMenusWithRole();
}
menuMapper
/**
* 根据角色获取菜单列表
* @return
*/
List<Menu> getMenusWithRole();
menuMapper.xml
根据登录之后的adminId在admin_role表中拿到rid,
根据rid在role表中拿到role信息。
<!-- 根据角色获取菜单列表-->
<select id="getMenusWithRole" resultType="com.wyc.mbg.pojo.Menu">
SELECT
m.*,
r.id AS rid,
r.name AS rname,
r.nameZh AS rnameZh
FROM
t_menu m,
t_menu_role mr,
t_role r
WHERE
m.id = mr.mid
AND r.id = mr.rid
ORDER BY
m.id
</select>
权限控制
component/CustomFilter
package com.wyc.config.component;
import com.wyc.mbg.pojo.Menu;
import com.wyc.mbg.pojo.Role;
import com.wyc.mbg.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
/**
* 权限控制
* @author:superBrother
* @create: 2022-07-04 16:35
* @Description: 根据请求url分析请求所需的角色
*/
@Component
public class CustomFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
private IMenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//获取请求的url
String requestUrl = ((FilterInvocation)object).getRequestUrl();
List<Menu> menus = menuService.getMenusWithRole();
for (Menu menu :menus){
//判断请求url与菜单角色是否匹配
if (antPathMatcher.match(menu.getUrl(),requestUrl)){
String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
return SecurityConfig.createList(str);
}
}
//没匹配的url默认登录即可访问
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
判断用户角色,判断登录的用户都有那些角色。
然后就可以根据请求的url角色和登录获取的角色进行比较,如果一直,用户就可以访问这些菜单资源。
第一步:修改pojo/Admin
@ApiModelProperty(value = "角色")
@TableField (exist = false)
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return authorities;
}
第二步:修改service
/**
* 根据用户id查询角色列表
* @param adminId
* @return
*/
List<Role> getRoles(Integer adminId);
serviceimpl
/**
* 根据用户id查询角色列表
* @param adminId
* @return
*/
@Override
public List<Role> getRoles(Integer adminId) {
return roleMapper.getRoles(adminId);
}
roleMapper
/**
* 根据用户id查询角色列表
* @param adminId
* @return
*/
List<Role> getRoles(Integer adminId);
roleMapper.xml
<!-- 根据用户id查询角色列表-->
<select id="getRoles" resultType="com.wyc.mbg.pojo.Role">
SELECT
r.id,
r.name,
r.nameZh
FROM
t_role AS r
LEFT JOIN
t_admin_role AS ar
ON r.id = ar.rid
WHERE ar.adminId = ${adminId}
</select>
修改登录方法,加上根据角色判断url方法,让每次登录完之后,就可以获取角色列表。
LoginController 修改这个获取当前用户信息,将role返回
@ApiOperation(value = "获取当前登录用户信息")
@GetMapping(value = "/admin/info")
public Admin getAdminInfo(Principal principal){
if (null==principal){
return null;
}
String username = principal.getName();
Admin admin = adminService.getAdminByUserName(username);
admin.setPassword(null);
//返回信息就会拿到角色
admin.setRoles(adminService.getRoles(admin.getId())); //修改
return admin;
}
修改登录,因为登录是通过userDetailsService.loadUserByUsername(username)方法,然后去SecurityConfig.
@Override
@Bean // @Bean注解,返回的对象交给IOC容器管理
public UserDetailsService userDetailsService() {
// 通过箭头函数重写 UserDetailsService 下的 loadUserByUsername()方法
return username -> { // 箭头代表将前面的参数username传递给后面的代码
Admin admin = adminService.getAdminByUserName(username);
if (null != admin) {
admin.setRoles(adminService.getRoles(admin.getId())); //修改
return admin;
}
// return null;
//抛出SpringSecurity提供的异常 UsernameNotFoundException
throw new UsernameNotFoundException("用户名或密码不正确!");
};
}
创建component/CustomUrlDecisionManager
package com.wyc.config.component;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 权限控制
* @author:superBrother
* @create: 2022-07-04 17:18
* @Description: 判断用户角色
*/
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
// 当前url所需角色
String needRole = configAttribute.getAttribute();
// 判断角色是否登录即可访问的角色,此角色在 CustomFilter 中设置
if ("ROLE_LOGIN".equals(needRole)) {
// 判断是否登录
if (authentication instanceof AnonymousAuthenticationToken) { // 如果是AnonymousAuthenticationToken,表示还没有登录
throw new AccessDeniedException("尚未登录,请登录!");
} else {
return;
}
}
// 判断用户角色是否为url所需角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return false;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
修改SecurityConfig,加入动态权限配置
先引入
@Autowired
private CustomFilter customFilter;
@Autowired
private CustomUrlDecisionManager customUrlDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 使用JWT,不需要csrf
http.csrf().disable()
// 基于token, 不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// .antMatchers("/login","/logout")
// .permitAll()
// 所有请求都要求认证
.anyRequest()
.authenticated()
//动态权限配置 添加
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilter);
return object;
}
})
.and()
// 禁用缓存
.headers()
.cacheControl();
权限管理
1.首先根据请求的url分析url需要那些角色,
2.根据登录的用户分析判断这个用户拥有那些角色,然后进行匹配,匹配的上,就让访问,如果不匹配,就告诉权限不足,去联系管理员。
职位管理
在pojo/position加上时间格式注解@JsonFormat(。
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private LocalDateTime createDate;
controller
@RestController
@RequestMapping("/system/basic/pos")
public class PositionController {
@Autowired
private PositionService positionService;
@ApiOperation(value = "获取所有职位信息")
@GetMapping("/")
public List<Position> getAllPosition() {
return positionService.list();
}
@ApiOperation(value = "添加职位")
@PostMapping("/")
public RespBean addPosition(@RequestBody Position position) {
position.setCreateDate(LocalDateTime.now());
if (positionService.save(position)) {
return RespBean.success("添加成功!");
}
return RespBean.error("添加失败!");
}
@ApiOperation(value = "更新职位信息")
@PutMapping("/")
public RespBean updatePosition(@RequestBody Position position) {
position.setCreateDate(LocalDateTime.now());
if (positionService.updateById(position)) {
return RespBean.success("更新成功!");
}
return RespBean.error("更新失败!");
}
@ApiOperation(value = "删除单条职位信息")
@DeleteMapping("/{id}")
public RespBean deletePosition(@PathVariable Integer id) {
if (positionService.removeById(id)) {
return RespBean.success("删除成功!");
}
return RespBean.error("删除失败!");
}
@ApiOperation(value = "批量删除职位信息")
@DeleteMapping("/")
public RespBean deletePositionByIds(Integer[] ids) {
if (positionService.removeByIds(Arrays.asList(ids))) {
return RespBean.success("删除成功!");
}
return RespBean.error("删除失败!");
}
权限组角色
需要注意的是:添加角色时,要给角色拼接ROLE_这是springSecurity的格式。
package com.wyc.mbg.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wyc.common.RespBean;
import com.wyc.mbg.pojo.MenuRole;
import com.wyc.mbg.pojo.Role;
import com.wyc.mbg.service.IMenuRoleService;
import com.wyc.mbg.service.IMenuService;
import com.wyc.mbg.service.IRoleService;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author:superBrother
* @create: 2022-07-05 07:01
* @Description: 权限组
*/
@RestController
@RequestMapping("/system/basic/permiss")
public class PermissionController {
@Autowired
private IRoleService roleService;
@Autowired
private IMenuService menuService;
@Autowired
private IMenuRoleService menuRoleService;
@ApiModelProperty(value = "获取所有角色")
@GetMapping("/")
public List<Role> getAllRoles(){
return roleService.list();
}
@ApiModelProperty(value = "添加角色")
@PostMapping("/role")
public RespBean addRole(@RequestBody Role role){
//用来判断传入的Name前面是否有ROLE,springSecurity定义的格式,如果没有,就加上这个格式。
if (!role.getName().startsWith("ROLE_")){
role.setName("ROLE_"+role.getName());
}
if (roleService.save(role)){
return RespBean.success("添加成功!");
}
return RespBean.error("添加失败!");
}
@ApiModelProperty(value = "更新角色")
@PutMapping("/")
public RespBean updateRole(@RequestBody Role role){
if (roleService.updateById(role)){
return RespBean.success("更新成功!");
}
return RespBean.error("更新失败!");
}
@ApiModelProperty(value = "删除角色")
@DeleteMapping("/role/{rid}")
public RespBean deleteRoleById(@PathVariable Integer rid){
if (roleService.removeById(rid)){
return RespBean.success("删除成功!");
}
return RespBean.error("删除失败!");
}
@ApiModelProperty(value = "查询所有菜单")
@GetMapping("menus")
public RespBean getAllMenus(){
return menuService.getAllMenus();
}
@ApiModelProperty(value = "根据角色id查询菜单id")
@GetMapping("/mid/{rid}")
public List<Integer> getMidByRid(@PathVariable Integer rid){
return menuRoleService.list(new QueryWrapper<MenuRole>().eq("rid",rid))
.stream()
.map(MenuRole::getMid)
.collect(Collectors.toList());
}
@ApiModelProperty(value = "更新角色菜单")
@PutMapping()
public RespBean updateRoleMenu(Integer rid,Integer[] mids){
return menuRoleService.updateMenuRole(rid,mids);
}
}
存储过程
存储过程的创建和调用
储存过程就是具有名字的一段代码,用来完成一个特定的功能。
创建的存储过程保存在数据库的数据字典中。
分页配置
common/MyBatisPlusConfig
package com.wyc.common;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author:superBrother
* @create: 2022-07-07 08:10
* @Description: 分页配置
*/
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
// 原理就是你查询的时候被这里拦截,然后加上 limit,实现分页查询
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setOverflow(false);
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor;
}
}
分页公共返回对象
common/RespBean
package com.wyc.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author:superBrother
* @create: 2022-07-07 08:12
* @Description: 分页公共返回对象
*/
@Data // Getter/Setter方法
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 全参构造
public class RespPageBean {
/**
* 总条数
*/
private Long total;
/**
* 数据 List
*/
private List<?> data;
}
时间转换
converter/DateConverter
package com.wyc.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* @author:superBrother
* @create: 2022-07-07 08:15
* @Description: 日期转换
*/
@Component
public class DateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String source) {
try { //防止出现问题,加异常 选中代码然后Ctrl + Alt + T
return LocalDate.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
在employee实体类中,加上@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")注解。
员工操作
首先修改pojo类
package com.wyc.mbg.pojo;
import cn.afterturn.easypoi.excel.annotation.ExcelEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import java.time.LocalDate;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
*
* </p>
*
* @author superBrother
* @since 2022-07-02
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_employee")
@ApiModel(value="Employee对象", description="")
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "员工编号")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "员工姓名")
private String name;
@ApiModelProperty(value = "性别")
private String gender;
@ApiModelProperty(value = "出生日期")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private LocalDate birthday;
@ApiModelProperty(value = "身份证号")
private String idCard;
@ApiModelProperty(value = "婚姻状况")
private String wedlock;
@ApiModelProperty(value = "民族")
private Integer nationId;
@ApiModelProperty(value = "籍贯")
private String nativePlace;
@ApiModelProperty(value = "政治面貌")
private Integer politicId;
@ApiModelProperty(value = "邮箱")
private String email;
@ApiModelProperty(value = "电话号码")
private String phone;
@ApiModelProperty(value = "联系地址")
private String address;
@ApiModelProperty(value = "所属部门")
private Integer departmentId;
@ApiModelProperty(value = "职称ID")
private Integer jobLevelId;
@ApiModelProperty(value = "职位ID")
private Integer posId;
@ApiModelProperty(value = "聘用形式")
private String engageForm;
@ApiModelProperty(value = "最高学历")
private String tiptopDegree;
@ApiModelProperty(value = "所属专业")
private String specialty;
@ApiModelProperty(value = "毕业院校")
private String school;
@ApiModelProperty(value = "入职日期")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private LocalDate beginDate;
@ApiModelProperty(value = "在职状态")
private String workState;
@ApiModelProperty(value = "工号")
private String workID;
@ApiModelProperty(value = "合同期限")
private Double contractTerm;
@ApiModelProperty(value = "转正日期")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private LocalDate conversionTime;
@ApiModelProperty(value = "离职日期")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private LocalDate notWorkDate;
@ApiModelProperty(value = "合同起始日期")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private LocalDate beginContract;
@ApiModelProperty(value = "合同终止日期")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private LocalDate endContract;
@ApiModelProperty(value = "工龄")
private Integer workAge;
@ApiModelProperty(value = "工资账套ID")
private Integer salaryId;
//////////////// 以下为外键字段 //////////////////////
@ApiModelProperty(value = "民族")
@TableField(exist = false) // 数据库中不存在需要加上这个注解
@ExcelEntity(name = "民族") ////
private Nation nation;
@ApiModelProperty(value = "政治面貌")
@TableField(exist = false)
@ExcelEntity(name = "政治面貌") ////
private PoliticsStatus politicsStatus;
@ApiModelProperty(value = "部门")
@TableField(exist = false)
@ExcelEntity(name = "部门") ////
private Department department;
@ApiModelProperty(value = "职称")
@TableField(exist = false)
@ExcelEntity(name = "职称") ////
private Joblevel joblevel;
@ApiModelProperty(value = "职位")
@TableField(exist = false)
@ExcelEntity(name = "职位") ////
private Position position;
@ApiModelProperty(value = "工资账套")
@TableField(exist = false)
private Salary salary;
}
查询controller
package com.wyc.mbg.controller;
import com.wyc.common.RespPageBean;
import com.wyc.mbg.pojo.Employee;
import com.wyc.mbg.service.IEmployeeService;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
/**
* <p>
* 前端控制器
* </p>
*
* @author superBrother
* @since 2022-07-02
*/
@RestController
@RequestMapping("/employee/basic")
public class EmployeeController {
@Autowired
private IEmployeeService employeeService;
@ApiModelProperty(value = "获取所有员工(分页)")
@GetMapping("/")
public RespPageBean getEmployee(@RequestParam(defaultValue = "1") Integer currentPage,
@RequestParam(defaultValue = "10") Integer size,
Employee employee,
LocalDate[] beginDateScope){
return employeeService.getEmployeeByPage(currentPage,size,employee,beginDateScope);
}
}
impl
package com.wyc.mbg.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wyc.common.RespPageBean;
import com.wyc.mbg.pojo.Employee;
import com.wyc.mbg.mapper.EmployeeMapper;
import com.wyc.mbg.service.IEmployeeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
/**
* <p>
* 服务实现类
* </p>
*
* @author superBrother
* @since 2022-07-02
*/
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
/**
* 获取所有员工(分页)
* @param currentPage
* @param size
* @param employee
* @param beginDateScope
* @return
*/
@Override
public RespPageBean getEmployeeByPage(Integer currentPage, Integer size, Employee employee, LocalDate[] beginDateScope) {
//开启分页
Page<Employee> page = new Page<>(currentPage,size);
// IPage是国人开发的一个插件,用来做分页,可以点击去看源码(发现里面是中文的注释,因为是国人开发的)
IPage<Employee> employeeByPage = employeeMapper.getEmployeeByPage(page, employee, beginDateScope);
RespPageBean respPageBean = new RespPageBean(employeeByPage.getTotal(), employeeByPage.getRecords());
return respPageBean;
}
}
mapper
package com.wyc.mbg.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wyc.mbg.pojo.Employee;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
/**
* <p>
* Mapper 接口
* </p>
*
* @author superBrother
* @since 2022-07-02
*/
public interface EmployeeMapper extends BaseMapper<Employee> {
/**
* 获取所有员工(分页)
* @param page
* @param employee
* @param beginDateScope
*/
IPage<Employee> getEmployeeByPage(Page<Employee> page, @Param("employee") Employee employee, @Param("beginDateScope") LocalDate[] beginDateScope);
}
mapper.xml
<!-- 获取所有员工(分页) resultMap -->
<resultMap id="EmployeeInfo" type="com.wyc.mbg.pojo.Employee" extends="BaseResultMap">
<!-- 这里是对象 -->
<!-- 民族 -->
<association property="nation" javaType="com.wyc.mbg.pojo.Nation">
<id column="nid" property="id"/>
<result column="nname" property="name"/>
</association>
<!-- 政治面貌 -->
<association property="politicsStatus" javaType="com.wyc.mbg.pojo.PoliticsStatus">
<id column="pid" property="id"/>
<result column="pname" property="name"/>
</association>
<!-- 部门 -->
<association property="department" javaType="com.wyc.mbg.pojo.Department">
<id column="did" property="id"/>
<result column="dname" property="name"/>
</association>
<!-- 职称 -->
<association property="joblevel" javaType="com.wyc.mbg.pojo.Joblevel">
<id column="jid" property="id"/>
<result column="jname" property="name"/>
</association>
<!-- 职位 -->
<association property="position" javaType="com.wyc.mbg.pojo.Position">
<id column="posid" property="id"/>
<result column="posname" property="name"/>
</association>
</resultMap>
<!-- 获取所有员工(分页)-->
<select id="getEmployeeByPage" resultMap="EmployeeInfo">
SELECT
e.*,
n.id AS nid,
n.`name` AS nname,
p.id AS pid,
p.`name` AS pname,
d.id AS did,
d.`name` AS dname,
j.id AS jid,
j.`name` AS jname,
pos.id AS posid,
pos.`name` AS posname
FROM
t_employee e,
t_nation n,
t_politics_status p,
t_department d,
t_joblevel j,
t_position pos
WHERE
e.nationId = n.id
AND e.politicId = p.id
AND e.departmentId = d.id
AND e.jobLevelId = j.id
AND e.posId = pos.id
<if test="null!=employee.name and ''!=employee.name">
AND e.`name` LIKE CONCAT( '%', #{employee.name}, '%' )
</if>
<if test="null!=employee.politicId">
AND e.politicId = #{employee.politicId}
</if>
<if test="null!=employee.nationId">
AND e.nationId = #{employee.nationId}
</if>
<if test="null!=employee.jobLevelId">
AND e.jobLevelId = #{employee.jobLevelId}
</if>
<if test="null!=employee.posId">
AND e.posId = #{employee.posId}
</if>
<if test="null!=employee.engageForm and ''!=employee.engageForm">
AND e.engageForm = #{employee.engageForm}
</if>
<if test="null!=employee.departmentId">
AND e.departmentId = #{employee.departmentId}
</if>
<if test="null!=beginDateScope and 2==beginDateScope.length">
AND e.beginDate BETWEEN #{beginDateScope[0]} AND #{beginDateScope[1]}
</if>
ORDER BY
e.id
</select>
数据的导入导出
导出
Controller
@ApiOperation(value = "导出员工数据")
@GetMapping(value = "/export", produces = "application/octet-stream")
public void exportEmployee(HttpServletResponse response) { // 使用流的方式传出去
List<Employee> list = employeeService.getEmployee(null); // 因为查询所有所以传入null
// 导出数据,ExcelType.HSSF兼容性好,所以就用这个 /////////////////
ExportParams params = new ExportParams("员工表", "员工表", ExcelType.HSSF); // 使用03版的Excel,兼容性好
Workbook workbook = ExcelExportUtil.exportExcel(params, Employee.class, list);
ServletOutputStream out = null;
try {
// 流形式
response.setHeader("content-type", "application/octet-stream");
// 防止中文乱码
response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode("员工表.xls", "UTF-8"));
out = response.getOutputStream();
workbook.write(out);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
server
/**
* 查询员工
* @param id
* @return
*/
List<Employee> getEmployee(Integer id);
}
impl
/**
* 查询员工
* @param id
* @return
*/
@Override
public List<Employee> getEmployee(Integer id) {
return employeeMapper.getEmployee(id);
}
mapper
/**
* 查询员工
* @param id
* @return
*/
List<Employee> getEmployee(Integer id);
xml
<!-- 查询员工-->
<select id="getEmployee" resultMap="EmployeeInfo">
SELECT
e.*,
n.id AS nid,
n.`name` AS nname,
p.id AS pid,
p.`name` AS pname,
d.id AS did,
d.`name` AS dname,
j.id AS jid,
j.`name` AS jname,
pos.id AS posid,
pos.`name` AS posname
FROM
t_employee e,
t_nation n,
t_politics_status p,
t_department d,
t_joblevel j,
t_position pos
WHERE
e.nationId = n.id
AND e.politicId = p.id
AND e.departmentId = d.id
AND e.jobLevelId = j.id
AND e.posId = pos.id
<if test="null!=id">
AND e.id = #{id}
</if>
ORDER BY
e.id
</select>
导入
Controller
@ApiOperation(value = "导入员工数据")
@PostMapping("/import")
public RespBean importEmployee(MultipartFile file) { // 需要什么就在该方法的参数中加上去,如Request、Response等
ImportParams params = new ImportParams();
// 去掉标题行,就是从第二行开始
params.setTitleRows(1);
// 通过查询数据库获取到所有的民族,然后在下面匹配获取到Id,使用MyBatisPlus提供的list()方法获取表数据
List<Nation> nationList = nationService.list();
List<PoliticsStatus> politicsStatusList = politicsStatusService.list();
List<Department> departmentList = departmentService.list();
List<Joblevel> joblevelList = joblevelService.list();
List<Position> positionList = positionService.list();
try {
List<Employee> list = ExcelImportUtil.importExcel(file.getInputStream(), Employee.class, params); // 获取Excel表格中的数据
list.forEach(employee -> { // lambda表达式
// 重点是要通过导入的内容转换成Id
/// new Nation(employee.getNation().getName()) --> 通过导入的数据生成一个Nation对象
/// nationList.indexOf(new Nation(employee.getNation().getName())) --> 通过生成的Nation对象匹配到对应的下标
/// nationList.get(nationList.indexOf(new Nation(employee.getNation().getName()))) --> 通过下标再获取到数据查出并匹配的Nation对象
/// nationList.get(nationList.indexOf(new Nation(employee.getNation().getName()))).getId() --> 然后获取对应的Id
// 民族id
employee.setNationId(nationList.get(nationList.indexOf(new Nation(employee.getNation().getName()))).getId());
// 政治面貌id
employee.setPoliticId(politicsStatusList.get(politicsStatusList.indexOf(new PoliticsStatus(employee.getPoliticsStatus().getName()))).getId());
// 部门id
employee.setDepartmentId(departmentList.get(departmentList.indexOf(new Department(employee.getDepartment().getName()))).getId());
// 职称id
employee.setJobLevelId(joblevelList.get(joblevelList.indexOf(new Joblevel(employee.getJoblevel().getName()))).getId());
// 职位id
employee.setPosId(positionList.get(positionList.indexOf(new Position(employee.getPosition().getName()))).getId());
});
if (employeeService.saveBatch(list)) { // 插入到数据库中
return RespBean.success("导入成功!");
}
} catch (Exception e) {
e.printStackTrace();
}
return RespBean.error("导入失败!");
}
邮件发送
创建module。maven项目选择quelistart。
导入依赖
<dependencies>
<!--rabbitmq 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--mail 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!--thymeleaf 依赖,用来专门写邮件的模板-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--server 依赖,*************这个依赖就有点意思了********-->
<dependency>
<groupId>com.wyc</groupId>
<artifactId>yeb-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
配置文件
resources/config/application
server:
# 端口
port: 8082
spring:
# 邮件配置163
mail:
# 服务器地址
host: smtp.163.com
# 协议
protocol: smtp
# 编码格式
default-encoding: utf-8
# 授权码
password: WCXOTSJNYJKMTWHK
# 发送者邮箱地址
username: superbroo@163.com
# 端口号
port: 25
#spring:
# mail:
# username: superbroo@qq.com
# password: wjvxdfodcvqcgbce
# host: smtp.qq.com
# port: 465
# properties:
# mail:
# transport:
# protocol: smtp
# smtp:
# socketFactory:
# class: javax.net.ssl.SSLSocketFactory
# port: ${spring.mail.port}
# auth: true
# starttls:
# enable: true
# required: true
# rabbitmq配置
rabbitmq:
# 用户名
username: guest
# 密码
password: guest
# 服务器地址
host: 120.26.222.131
# host: 192.168.15.181
# 端口
port: 5672
# 消息确认回调
publisher-confirm-type: correlated
# 消息失败回调
publisher-returns: true
# redis配置
redis:
# 超时时间
timeout: 10000ms
# 服务器地址(学习时用的是虚拟机的ip地址)
host: 127.0.0.1
# 服务器端口
port: 6379
# 选择的数据库
database: 0
# 密码(学习时,没有密码就不用配了)
# password: 123456
# 使用lettuce操作redis(还有Jedis也可以操作Redis)
lettuce:
pool:
# 最大连接数,默认是8
max-active: 1024
# 最大连接阻塞等待时间,默认-1
max-wait: 10000ms
# 最大空闲连接,默认是8
max-idle: 200
# 最小空闲连接,默认是0
min-idle: 5
邮件模板
resource/templates/mail.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>入职欢迎邮件</title>
</head>
<body>
欢迎 <span th:text="${name}"></span> 加入 XXXX 大家庭,您的入职信息如下:
<table border="1">
<tr>
<td>姓名</td>
<td th:text="${name}"></td>
</tr>
<tr>
<td>职位</td>
<td th:text="${posName}"></td>
</tr>
<tr>
<td>职称</td>
<td th:text="${joblevelName}"></td>
</tr>
<tr>
<td>部门</td>
<td th:text="${departmentName}"></td>
</tr>
</table>
<p>我们公司的工作忠旨是严格,创新,诚信,您的加入将为我们带来新鲜的血液,带来创新的思维,以及为
我们树立良好的公司形象!希望在以后的工作中我们能够齐心协力,与时俱进,团结协作!同时也祝您在本公
司,工作愉快,实现自己的人生价值!希望在未来的日子里,携手共进!</p>
</body>
</html>
主启动类
mail/MailApplication
package com.wyc.mail;
import com.wyc.common.MailConstants;
import org.springframework.amqp.core.Queue;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* @author superBrother
* @create 2022/7/2
* @Description
*/
// 去掉数据源的配置,否则启动的时候该启动类会报没有数据源的错误
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MailApplication {
public static void main(String[] args) {
SpringApplication.run(MailApplication.class, args);
}
// 创建消息队列 名字是 mail.welcome
@Bean
public org.springframework.amqp.core.Queue queue() {
return new Queue(MailConstants.MAIL_QUEUE_NAME); // 改为定义的常量
}
}
消息接受者
mail/MailReceiver
package com.wyc.mail;
import com.rabbitmq.client.Channel;
import com.wyc.common.MailConstants;
import com.wyc.mbg.pojo.Employee;
//import com.wyc.mbg.pojo.MailConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.util.Date;
/**
* @author:superBrother
* @create: 2022-07-07 20:46
* @Description: 消息接受者
*/
@Component
public class MailReceiver {
// 准备日志打印,使用slf4j日志打印,门面模式
private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class);
// 下面三个都是跟邮件相关的东西
@Autowired
private JavaMailSender javaMailSender; // 注入邮件发送对象
@Autowired
private MailProperties mailProperties; // 邮件的属性配置,在application.yml中
@Autowired
private TemplateEngine templateEngine; // thymeleaf模板引擎
@Autowired
private RedisTemplate redisTemplate;
/**
* 因为使用了 rabbitTemplate.convertAndSend("mail.welcome", emp); 发送消息
* 所以在这里就要监听他的RoutingKey: mail.welcome
*
* @param message
*/
@RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME) // 改为定义的常量
public void handler(Message message, Channel channel) {
Employee employee = (Employee) message.getPayload();
MessageHeaders headers = message.getHeaders();
// 消息序号
long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 里面的参数是固定的
String msgId = (String) headers.get("spring_returned_message_correlation");
HashOperations hashOperations = redisTemplate.opsForHash();
try {
if (hashOperations.entries("mail_log").containsKey(msgId)) {
LOGGER.error("消息已经被消费============>{}", msgId);
/**
* 手动确认消息
* tag:消息序号
* multiple:是否确认多条
*/
channel.basicAck(tag, false);
return;
}
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg);
// 发件人
helper.setFrom(mailProperties.getUsername()); // 获取在application.yml中配置邮件的的信息
// 收件人
helper.setTo(employee.getName());
// 邮件主题
helper.setSubject("入职欢迎邮件");
// 发送日期
helper.setSentDate(new Date());
// 邮件内容
Context context = new Context(); // thymeleaf 的Context
context.setVariable("name", employee.getName());
context.setVariable("posName", employee.getPosition().getName());
context.setVariable("joblevelName", employee.getJoblevel().getName());
context.setVariable("departmentName", employee.getDepartment().getName());
String mail = templateEngine.process("mail", context); // 这里的mail要与mail.html对应
// 设置邮件的发送模板
helper.setText(mail, true); // 这里要加上true,不然显示的是html的源码
// 发送邮件
javaMailSender.send(msg);
LOGGER.info("邮件发送成功!");
// 将消息id存入redis
hashOperations.put("mail_log", msgId, "OK");
// 手动确认消息
channel.basicAck(tag, false);
} catch (Exception e) {
/**
* 手动确认消息
* tag:消息序号
* multiple:是否确认多条
* requeue:是否要退回到队列
*/
try {
channel.basicNack(tag, false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
LOGGER.error("邮件发送失败=======>{}", e.getMessage());
}
}
}
消息状态,常量
yeb-server/common/MailConstants
package com.wyc.common;
/**
* @author:superBrother
* @create: 2022-07-07 21:42
* @Description: 消息状态
*/
public class MailConstants {
// 消息投递中
public static final Integer DELIVERING = 0;
// 消息投递成功
public static final Integer SUCCESS = 1;
// 消息投递失败
public static final Integer FAILURE = 2;
// 最大重试次数
public static final Integer MAX_TRY_COUNT = 3;
// 消息超时时间
public static final Integer MSG_TIMEOUT = 1;
// 队列
public static final String MAIL_QUEUE_NAME = "mail.queue";
// 交换机
public static final String MAIL_EXCHANGE_NAME = "mail.exchange";
// 路由键
public static final String MAIL_ROUTING_KEY_NAME = "mail.routing.key";
}
RabbitMQConfig配置类
package com.wyc.config;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.wyc.common.MailConstants;
import com.wyc.mbg.pojo.MailLog;
import com.wyc.mbg.service.IMailLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author:superBrother
* @create: 2022-07-07 21:55
* @Description: RabbitMQ配置类
*/
@Configuration
public class RabbitMQConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(RabbitMQConfig.class);
@Autowired
private CachingConnectionFactory cachingConnectionFactory;
@Autowired
private IMailLogService mailLogService;
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
/**
* 消息确认回调,确认消息是否到达broker
* data:消息唯一标识
* ack:确认结果
* cause:失败原因
*/
rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
String msgId = data.getId();
if (ack) {
LOGGER.info("{}=======>消息发送成功", msgId);
mailLogService.update(new UpdateWrapper<MailLog>().set("status", 1).eq("msgId", msgId));
} else {
LOGGER.error("{}=======>消息发送失败", msgId);
}
});
/**
* 消息失败回调,比如router不到queue时回调
* msg:消息主题
* repCode:响应码
* repText:相应描述
* exchange:交换机
* routingkey:路由键
*/
rabbitTemplate.setReturnCallback((msg, repCode, repText, exchange, routingkey) -> {
LOGGER.error("{}=======>消息发送queue时失败", msg.getBody());
});
return rabbitTemplate;
}
// 队列
@Bean
public Queue queue() {
return new Queue(MailConstants.MAIL_QUEUE_NAME);
}
// 交换机,路由模式
@Bean
public DirectExchange directExchange() {
return new DirectExchange(MailConstants.MAIL_EXCHANGE_NAME);
}
// 绑定
@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);
}
}
定时任务,在主启动类开启定时任务,不能忘记。yeb-server
@SpringBootApplication
@MapperScan("com.wyc.mbg.mapper")
@EnableScheduling //开启定时任务
//// swaggerUI界面切换第三步:在启动类加上 @EnableSwagger2 注解(前面两步修改后,能成功访问,这步就不需要做了)
@EnableSwagger2
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
yeb-server/wyc/task/MailTask
package com.wyc.task;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.wyc.common.MailConstants;
import com.wyc.mbg.pojo.Employee;
import com.wyc.mbg.pojo.MailLog;
import com.wyc.mbg.service.IEmployeeService;
import com.wyc.mbg.service.IMailLogService;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author:superBrother
* @create: 2022-07-08 08:06
* @Description: 邮件发送定时任务
*/
@Component
public class MailTask {
@Autowired
private IMailLogService mailLogService;
@Autowired
private IEmployeeService employeeService;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 邮件发送定时任务
*
* @Scheduled(cron = "0/10 * * * * ?") 表示 10秒执行一次
*/
@Scheduled(cron = "0/10 * * * * ?")
public void mailTask() {
List<MailLog> list = mailLogService.list(new QueryWrapper<MailLog>().eq("status", 0).lt("tryTime", LocalDateTime.now()));
list.forEach(mailLog -> {
// 如果重试次数超过3次,更新状态为投递失败,不再重试
if (3 <= mailLog.getCount()) {
mailLogService.update(new UpdateWrapper<MailLog>().set("status", 2).eq("msgId", mailLog.getMsgId()));
}
mailLogService.update(new UpdateWrapper<MailLog>().set("count", mailLog.getCount() + 1).set("updateTime", LocalDateTime.now()).set("tryTime", LocalDateTime.now().plusMinutes(MailConstants.MSG_TIMEOUT)).eq("msgId", mailLog.getMsgId()));
Employee emp = employeeService.getEmployee(mailLog.getEid()).get(0);
// 发送消息
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(mailLog.getMsgId()));
});
}
}
在线聊天
在yeb-server引入依赖
<!--websocket 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置websocker
config/WebSocketConfig
package com.wyc.config;
import com.wyc.config.component.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @author:superBrother
* @create: 2022-07-08 09:27
* @Description: WebSocket配置类
*/
@Configuration // 配置类必须要加上的注解
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 获取application.yml文件中配置的值
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
/**
* 添加这个 Endpoint,这样在网页可以通过 websocket 连接上服务
* 也就是我们配置 websocket 的服务地址,并且可以指定是否使用 socketJS,
* 前端一般会使用 socketJS 去连接我们的服务
*
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* 1.将 /ws/ep 路径注册为 stomp 的端点,用户连接了这个端点就可以进行 websocket 通讯,支持 socketJS
* 2.setAllowedOrigins("*"):允许跨域
* 3.withSockJS(): 支持 socketJS 访问
*/
registry.addEndpoint("/ws/ep").setAllowedOrigins("*").withSockJS();
}
/**
* 输入通道参数配置,使用了JWT令牌就需要在这里配置websocket
*
* @param registration
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() { // 类似内部类
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
// 判断是否为连接,如果是,需要获取token,并且设置用户对象
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// Auth-Token参数是前端发给我们的
String token = accessor.getFirstNativeHeader("Auth-Token");
if (!StringUtils.isEmpty(token)) {
String authToken = token.substring(tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
// token中存在用户名
if (!StringUtils.isEmpty(username)) {
// 登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 验证token是否有效,重新设置用户对象
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 这里别忘了设置
accessor.setUser(authenticationToken);
}
}
}
}
return message;
}
});
}
/**
* 配置消息代理
*
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 配置代理域,可以配置多个,配置代理目的地前缀为 /queue,可以在配置域上向客户端推送消息
registry.enableSimpleBroker("/queue");
}
}
websocker Controller
package com.wyc.mbg.controller;
import com.wyc.common.ChatMsg;
import com.wyc.mbg.pojo.Admin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
/**
* @author:superBrother
* @create: 2022-07-08 09:40
* @Description: websocket
*/
@Controller // 这里使用普通的@Controller而不是Restful风格的@RestController
public class WsController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/ws/chat") // 这里的请求地址也跟其他的不一样,使用的是 @MessageMapping()
public void handleMsg(Authentication authentication, ChatMsg chatMsg) {
Admin admin = (Admin) authentication.getPrincipal();
chatMsg.setFrom(admin.getUsername());
chatMsg.setFormNickName(admin.getName());
chatMsg.setDate(LocalDateTime.now());
// 发送消息
simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
}
}
消息类
package com.wyc.common;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* @author:superBrother
* @create: 2022-07-08 09:37
* @Description: 消息
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ChatMsg {
private String from;
private String to;
private String content;
private LocalDateTime date;
private String formNickName;
}
在线聊天 controller
package com.wyc.mbg.controller;
import com.wyc.mbg.pojo.Admin;
import com.wyc.mbg.service.IAdminService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author:superBrother
* @create: 2022-07-08 09:45
* @Description: 在线聊天
*/
@RestController
@RequestMapping("/chat")
public class ChatController {
@Autowired
private IAdminService adminService;
@ApiOperation(value = "获取所有操作员")
@GetMapping("/admin")
public List<Admin> getAllAdmins(String keywords) {
return adminService.getAllAdmins(keywords);
}
}
个人中心
更新当前用户信息
Controller
package com.wyc.mbg.controller;
import com.wyc.common.RespBean;
import com.wyc.mbg.pojo.Admin;
import com.wyc.mbg.service.IAdminService;
import com.wyc.util.FastDFSUtils;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* @author:superBrother
* @create: 2022-07-08 10:02
* @Description: 个人中心
*/
@RestController
public class AdminInfoController {
@Autowired
private IAdminService adminService;
@ApiOperation(value = "更新当前用户信息")
@PutMapping("/admin/info")
public RespBean updateAdmin(@RequestBody Admin admin, Authentication authentication){
// 使用MyBatisPlus提供的updateById()方法进行更新
if (adminService.updateById(admin)) {
// 每次更新之后需要重新设置 SpringSecurity的 Authentication 对象
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(admin, null, authentication.getAuthorities()));
return RespBean.success("更新成功!");
}
return RespBean.error("更新失败!");
}
@ApiOperation(value = "更新用户密码")
@PutMapping("/admin/pass")
public RespBean updateAdminPassword(@RequestBody Map<String, Object> info) {
String oldPass = (String) info.get("oldPass"); // 旧密码
String pass = (String) info.get("pass"); // 新密码
Integer adminId = (Integer) info.get("adminId"); // 用户Id,用于指定用户的更新
return adminService.updateAdminPassword(oldPass, pass, adminId);
}
@ApiOperation(value = "更新用户头像")
@PostMapping("/admin/userface")
public RespBean updateAdminUserFace(MultipartFile file, Integer id, Authentication authentication) {
// 使用fdfs的工具类上传文件
String[] filePath = FastDFSUtils.upload(file);
// 获取上传文件的路径
String url = FastDFSUtils.getTrackerUrl() + filePath[0] + "/" + filePath[1];
return adminService.updateAdminUserFace(url, id, authentication);
}
}
impl
package com.wyc.mbg.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.wyc.common.RespBean;
import com.wyc.config.component.JwtTokenUtil;
import com.wyc.mbg.mapper.AdminRoleMapper;
import com.wyc.mbg.mapper.RoleMapper;
import com.wyc.mbg.pojo.Admin;
import com.wyc.mbg.mapper.AdminMapper;
import com.wyc.mbg.pojo.AdminRole;
import com.wyc.mbg.pojo.Role;
import com.wyc.mbg.service.IAdminRoleService;
import com.wyc.mbg.service.IAdminService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wyc.util.AdminUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author superBrother
* @since 2022-07-02
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
@Resource
private AdminMapper adminMapper;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private RoleMapper roleMapper;
@Autowired
private AdminRoleMapper adminRoleMapper;
/**
* 登录之后返回token
* @param username
* @param password
* @param code
* @param request
* @return
*/
@Override
public RespBean login(String username, String password, String code, HttpServletRequest request) {
String captcha =(String) request.getSession().getAttribute("captcha"); //拿到验证码进行匹配
if (StringUtils.isEmpty(code)||!captcha.equalsIgnoreCase(code)){ //如果验证码错误,直接返回
return RespBean.error("验证码输入错误,请重新输入!");
}
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//判断如果userDetails为空,或者前端的密码和userDetails加密后的密码匹配不成功,则返回错误。
if (null == userDetails ||!passwordEncoder.matches(password,userDetails.getPassword())){
return RespBean.error("用户名或密码不正确!");
}
//判断账号是否被禁用
if(!userDetails.isEnabled()){
return RespBean.error("账号被禁用,请联系管理员!");
}
//更新security登录用户对象用户登录之后,将用户信息放到springSecurity全局中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//生成token
//判断都通过,通过userDetails拿到令牌
String token = jwtTokenUtil.generateToken(userDetails);
//返回一个map,里面包含请求头,和这个token,并将提示信息和状态码也一起返回
HashMap<String, String> tokenMap = new HashMap<>();
tokenMap.put("token",token);
tokenMap.put("tokenHead",tokenHead); //tokenHead通过@value注解来拿
return RespBean.success("登录成功",tokenMap);
}
/**
* 根据用户名获取用户信息
* @param username
* @return
*/
@Override
public Admin getAdminByUserName(String username) {
return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username",username).eq("enabled",true));
}
/**
* 根据用户id查询角色列表
* @param adminId
* @return
*/
@Override
public List<Role> getRoles(Integer adminId) {
return roleMapper.getRoles(adminId);
}
/**
* 获取所有操作员
* @param keywords
* @return
*/
@Override
public List<Admin> getAllAdmins(String keywords) {
return adminMapper.getAllAdmins(AdminUtils.getCurrentAdmin().getId(),keywords);
}
/**
* 更新操作员角色
* @param adminId
* @param rids
* @return
*/
@Override
@Transactional //因为这里涉及到了数据库的两个操作,所以加事务注解安全。
public RespBean updateAdminRole(Integer adminId, Integer[] rids) {
//在更新之前需要进行删除
adminRoleMapper.delete(new QueryWrapper<AdminRole>().eq("adminId",adminId));
Integer result = adminRoleMapper.addAdminRole(adminId,rids);
//如果受影响的行数是一样的,就更新成功。
if(rids.length == result){
return RespBean.success("更新成功!");
}
return RespBean.error("更新失败!");
}
/**
* 更新用户密码
* @param oldPass
* @param pass
* @param adminId
* @return
*/
@Override
public RespBean updateAdminPassword(String oldPass, String pass, Integer adminId) {
// 直接通过MyBatisPlus提供的方法selectById(),通过adminId获取用户信息对象
Admin admin = adminMapper.selectById(adminId);
// 前端传入的明文密码加密后,与数据库加密的密码比较
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 判断旧密码是否正确
if (encoder.matches(oldPass, admin.getPassword())) {
admin.setPassword(encoder.encode(pass));
int result = adminMapper.updateById(admin);
if (1 == result) {
return RespBean.success("更新成功!");
}
}
return RespBean.error("更新失败!");
}
/**
* 更新用户头像
* @param url
* @param id
* @param authentication
* @return
*/
@Override
public RespBean updateAdminUserFace(String url, Integer id, Authentication authentication) {
// 根据Id拿到Admin对象,直接使用MyBatisPlus提供的selectById()方法
Admin admin = adminMapper.selectById(id);
admin.setUserFace(url);
int result = adminMapper.updateById(admin); // 更新头像
if (1 == result) {
// 还需要做一个SpringSecurity全局对象的更新,比如更新登录的用户对象
Admin principal = (Admin) authentication.getPrincipal();
principal.setUserFace(url);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(admin, null, authentication.getAuthorities()));
return RespBean.success("更新成功!", url);
}
return RespBean.error("更新失败!");
}
}
更新密码 Controller
@ApiOperation(value = "更新用户密码")
@PutMapping("/admin/pass")
public RespBean updateAdminPassword(@RequestBody Map<String, Object> info) {
String oldPass = (String) info.get("oldPass"); // 旧密码
String pass = (String) info.get("pass"); // 新密码
Integer adminId = (Integer) info.get("adminId"); // 用户Id,用于指定用户的更新
return adminService.updateAdminPassword(oldPass, pass, adminId);
}
impl
/**
* 更新用户密码
* @param oldPass
* @param pass
* @param adminId
* @return
*/
@Override
public RespBean updateAdminPassword(String oldPass, String pass, Integer adminId) {
// 直接通过MyBatisPlus提供的方法selectById(),通过adminId获取用户信息对象
Admin admin = adminMapper.selectById(adminId);
// 前端传入的明文密码加密后,与数据库加密的密码比较
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 判断旧密码是否正确
if (encoder.matches(oldPass, admin.getPassword())) {
admin.setPassword(encoder.encode(pass));
int result = adminMapper.updateById(admin);
if (1 == result) {
return RespBean.success("更新成功!");
}
}
return RespBean.error("更新失败!");
}
自定义json反序列化
package com.wyc.config;
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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* @author:superBrother
* @create: 2022-07-08 10:15
* @Description: 自定义Authority解析器
*/
public class CustomAuthorityDeserializer extends JsonDeserializer {
/**
* 自定义Json序列化--hmf
*
* @param p
* @param ctxt
* @return
* @throws IOException
* @throws JsonProcessingException
*/
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
JsonNode jsonNode = mapper.readTree(p);
List<GrantedAuthority> grantedAuthorities = new LinkedList<>();
Iterator<JsonNode> elements = jsonNode.elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText()));
}
return grantedAuthorities;
}
}
更新头像
引入FastDFS依赖
<!-- 解决依赖导入不成功的问题:http://www.qishunwang.net/knowledge_show_129043.aspx -->
<!--FastDFS依赖,用于头像的更新-->
<dependency>
<groupId>org.csource</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.29-SNAPSHOT</version>
</dependency>
FastDFS配置
/config/fdfs_client.conf
#连接超时
connect_timeout = 2
#网络超时
network_timeout = 30
#编码格式
charset = UTF-8
#tracker端口号
http.tracker_http_port = 8080
#防盗链功能
http.anti_steal_token = no
#秘钥
http.secret_key = FastDFS1234567890
#tracker ip:端口号,【因为可以是分布式的,所以这里可以配置多个】
tracker_server = 192.168.1.105:22122
#连接池配置
connection_pool.enabled = true
connection_pool.max_count_per_entry = 500
connection_pool.max_idle_time = 3600
connection_pool.max_wait_time_in_ms = 1000
FastDFS配置类
package com.wyc.util;
import org.csource.fastdfs.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @author:superBrother
* @create: 2022-07-08 10:32
* @Description: FastDFS工具类
*/
public class FastDFSUtils {
private static Logger logger = LoggerFactory.getLogger(FastDFSUtils.class);
/**
* 初始化客户端
* ClientGlobal.init 读取配置文件,并初始化对应的属性
*/
static {
try {
String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
ClientGlobal.init(filePath);
} catch (Exception e) {
logger.error("初始化FastDFS失败", e.getMessage());
}
}
/**
* 上传文件
*
* @param file
* @return
*/
public static String[] upload(MultipartFile file) {
String name = file.getOriginalFilename();
logger.info("文件名:", name);
StorageClient storageClient = null;
String[] uploadResults = null;
try {
//获取storage客户端
storageClient = getStorageClient();
//上传
uploadResults = storageClient.upload_file(file.getBytes(), name.substring(name.lastIndexOf(".") + 1),
null);
} catch (Exception e) {
logger.error("上传文件失败", e.getMessage());
}
if (null == uploadResults) {
logger.error("上传失败", storageClient.getErrorCode());
}
return uploadResults;
}
/**
* 获取文件信息
*
* @param groupName
* @param remoteFileName
* @return
*/
public static FileInfo getFileInfo(String groupName, String remoteFileName) {
StorageClient storageClient = null;
try {
storageClient = getStorageClient();
return storageClient.get_file_info(groupName, remoteFileName);
} catch (Exception e) {
logger.error("文件信息获取失败", e.getMessage());
}
return null;
}
/**
* 下载文件
*
* @param groupName
* @param remoteFileName
* @return
*/
public static InputStream downFile(String groupName, String remoteFileName) {
StorageClient storageClient = null;
try {
storageClient = getStorageClient();
byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
InputStream inputStream = new ByteArrayInputStream(fileByte);
return inputStream;
} catch (Exception e) {
logger.error("文件下载失败", e.getMessage());
}
return null;
}
/**
* 删除文件
*
* @param groupName
* @param remoteFileName
*/
public static void deleteFile(String groupName, String remoteFileName) {
StorageClient storageClient = null;
try {
storageClient = getStorageClient();
storageClient.delete_file(groupName, remoteFileName);
} catch (Exception e) {
logger.error("文件删除失败", e.getMessage());
}
}
/**
* 生成storage客户端
*
* @return
* @throws IOException
*/
private static StorageClient getStorageClient() throws IOException {
TrackerServer trackerServer = getTrackerServer();
StorageClient storageClient = new StorageClient(trackerServer, null);
return storageClient;
}
/**
* 生成tracker服务器
*
* @return
* @throws IOException
*/
private static TrackerServer getTrackerServer() throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getTrackerServer();
return trackerServer;
}
/**
* 获取文件路径
*
* @return
*/
public static String getTrackerUrl() {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = null;
StorageServer storeStorage = null;
try {
trackerServer = trackerClient.getTrackerServer();
storeStorage = trackerClient.getStoreStorage(trackerServer);
} catch (Exception e) {
logger.error("文件路径获取失败", e.getMessage());
}
// 这里的端口号 8888 也需要根据你的具体配置来写
return "http://" + storeStorage.getInetSocketAddress().getHostString() + ":8888/";
}
}
更新头像Controller
@ApiOperation(value = "更新用户头像")
@PostMapping("/admin/userface")
public RespBean updateAdminUserFace(MultipartFile file, Integer id, Authentication authentication) {
// 使用fdfs的工具类上传文件
String[] filePath = FastDFSUtils.upload(file);
// 获取上传文件的路径
String url = FastDFSUtils.getTrackerUrl() + filePath[0] + "/" + filePath[1];
return adminService.updateAdminUserFace(url, id, authentication);
}
更新头像impl
/**
* 更新用户头像
* @param url
* @param id
* @param authentication
* @return
*/
@Override
public RespBean updateAdminUserFace(String url, Integer id, Authentication authentication) {
// 根据Id拿到Admin对象,直接使用MyBatisPlus提供的selectById()方法
Admin admin = adminMapper.selectById(id);
admin.setUserFace(url);
int result = adminMapper.updateById(admin); // 更新头像
if (1 == result) {
// 还需要做一个SpringSecurity全局对象的更新,比如更新登录的用户对象
Admin principal = (Admin) authentication.getPrincipal();
principal.setUserFace(url);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(admin, null, authentication.getAuthorities()));
return RespBean.success("更新成功!", url);
}
return RespBean.error("更新失败!");
}
查询网址ip:http://tool.chinaz.com/dns/
云E办前端
前期项目搭建。
创建utils,编写api.js文件。
下载相应的插件
package.json
{
"name": "yeb",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.24.0",
"core-js": "^3.6.5",
"element-ui": "^2.15.6",
"vue": "^2.6.11",
"vue-form": "^4.10.3",
"vue-router": "^3.2.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
import axios from "axios";
import { Message } from "element-ui";
// import { config } from "vue/types/umd";
import router from '../router';
//请求拦截器
axios.interceptors.request.use(config => {
//如果存在token,请求携带这个token
if (window.sessionStorage.getItem('tokenStr')) { //先判断是否有token,请求有没有拿到token
config.headers['Authorization'] = window.sessionStorage.getItem('tokenStr'); //将存储的token赋值给config.headers的key['Authorization']
}
return config;
}, error => {
console.log(error);
})
//响应拦截器
axios.interceptors.response.use(success => {
//业务逻辑错误,已经成功调到后端接口
if (success.status && success.status == 200) {
if (success.data.code == 500 || success.data.code == 401 || success.data.code == 403) { //code是这几种,代表逻辑错误
Message.error({ message: success.data.message }); //message:键 success.data.message:值,后端接口返回的信息
return;
}
if (success.data.message) { //请求成功,但是要判断接口请求成功是否有返回提示信息,如果有就将提示信息返回到message对象中
Message.success({ message: success.data.message });
}
}
return success.data; //将拿到的数据返回出去。
}, error => { //没有调到后端接口,直接就报错了,可能服务器挂断等原因
if (error.response.code == 504 || error.response.code == 404) {
Message.error({ message: '服务器被吃了( ╯□╰ )' });
} else if (error.response.code == 403) {
Message.error({ message: '权限不足,请联系管理员!' });
} else if (error.response.code == 401) {
Message.error({ message: '尚未登录,请登录' });
router.replace(location, '/'); //如果是未登录,就跳转到登录页面去登录
} else {
if (error.response.data.message) { //判断有没有错误响应的信息
Message.error({ message: error.response.data.message }); //有响应的信息赋值出去
} else {
Message.error({ message: '未知错误!' }); //错误信息都没有,直接报错
}
}
return;
})
let base = ''; //前置路径,一般大项目会加
//传送json格式的post请求
export const postRequest = (url, params) => {
return axios({
method: 'post',
url: `${base}${url}`,
data: params
})
}
//传送json格式的put请求
export const putRequest = (url, params) => {
return axios({
method: 'put',
url: `${base}${url}`,
data: params
})
}
//传送json格式的get请求
export const getRequest = (url, params) => {
return axios({
method: 'get',
url: `${base}${url}`,
data: params
})
}
//传送json格式的delete请求
export const deleteRequest = (url, params) => {
return axios({
method: 'delete',
url: `${base}${url}`,
data: params
})
}
创建vue.config.js解决跨域,代理问题
let proxyObj = {}
proxyObj['/'] = {
//websocket 普通接口代理
ws: false,
//目标地址
target: 'http://localhost:8081/',
//发送请求头host会被设置target
changeOrigin: true,
//不重写请求地址
pathReWrite: {
'^/': '/'
}
}
module.exports = {
devServer: {
host: 'localhost',
port: 8080,
proxy: proxyObj
}
}
main.js
全局的引用,如,将api的所有请求全局引入,然后使用,其他页面直接this.就可直接使用,不需要每个页面引入。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import VueForm from 'vue-form'
import { postRequest } from "./utils/api"
import { putRequest } from "./utils/api"
import { getRequest } from "./utils/api"
import { deleteRequest } from "./utils/api"
Vue.config.productionTip = false
Vue.use(ElementUI);
Vue.use(VueForm);
//插件形式使用请求
Vue.prototype.postRequest = postRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.getRequest = getRequest;
Vue.prototype.deleteRequest = deleteRequest;
new Vue({
router,
render: h => h(App)
}).$mount('#app')
router/index.js
编写页面请求路径
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import Home from '../views/Home.vue'
import Test1 from '../views/test1.vue'
import Test2 from '../views/test2.vue'
Vue.use(VueRouter)
const routes = [{
path: '/',
name: 'Login',
component: Login
},
{
path: '/Home',
name: 'Home',
component: Home,
//子路由,在home页面显示
children: [{
path: '/Test1',
name: 'Test1',
component: Test1
},
{
path: '/Test2',
name: 'Test2',
component: Test2
},
]
},
]
const router = new VueRouter({
routes
})
export default router
view文件夹下页面
登录login.vue
<template>
<div>
<el-form ref="loginForm"
v-loading="loading"
element-loading-text="正在登录..."
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0,0,0,0.8)"
:rules="rules" :model="loginForm"
label-width="80px"
class="loginContainer">
<h2 class="logintitle">系统登录</h2>
<el-form-item prop="username">
<el-input type="text" auto-complete="false" v-model="loginForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" auto-complete="false" v-model="loginForm.password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item prop="code">
<el-input type="text" auto-complete="false" v-model="loginForm.code" placeholder="点击图片,更换验证码" style="width:200px;margin-right:5px"></el-input>
<img :src="captchaUrl" @click="updateCaptcha">
</el-form-item>
<el-checkbox v-model="checked" class="loginRemember">记住我</el-checkbox>
<el-button type="primary" style="width:100%" @click="submitLogin">登录</el-button>
</el-form>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
captchaUrl:'/captcha?time='+new Date(),
loginForm:{
username:'admin',
password:'123456',
code:''
},
loading:false, //false,没有加载,ture,正在加载中
checked:true,
rules:{
username:[{required:true, message:'请输入用户名',trigger:'blur'}],
password:[{required:true, message:'请输入用户名',trigger:'blur'}],
code:[{required:true, message:'请输入用户名',trigger:'blur'}],
},
};
},
methods: {
updateCaptcha(){
this.captchaUrl ='/captcha?time='+new Date(); //确保验证码随时刷新
console.log("------------");
console.log(this.captchaUrl);
},
submitLogin(){
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true; //登录时正在加载
//this.postRequser通过this全局调用请求,在main.js全局引用api请求。
this.postRequest('/login',this.loginForm).then(resp=>{
console.log(resp);
if(resp){
this.loading = false; //登录成功取消加载
const tokenStr = resp.obj.tokenHead+resp.obj.token; //拿到请求token
//存储用户token到sessionStorage.
window.sessionStorage.setItem('tokenStr',tokenStr);
//跳转页面
this.$router.replace('/home') //replace 回退按钮回不去之前的页面,要是push就可以回去。
}else{
this.loading = false; //登录成功取消加载 登录失败也要取消加载 //自己写的else
}
})
} else {
this.$message.error('请输入所有信息');
return false;
}
});
}
}
};
</script>
<style >
.loginContainer{
border-radius: 15px;
background-clip: padding-box;
margin: 180px auto;
width:350px;
padding: 15px 35px 15px 35px;
background: #fff;
border:1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.logintitle{
margin:0px auto 40 auto;
text-align: center;
}
.loginRemember{
text-align: left;
margin:0px 0px 15px 0px;
}
.el-form-item__content{
display: flex;
align-content: center;
}
</style>
主页面Home.vue
安装vuex,保存数据
在src目录下新建store文件,编辑index.js,并在main.js引入,在new Vue中添加store.
store/index.js
import Vue from "vue";
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
routes: []
},
//同步,改变state
mutations: {
initRoutes(state, data) {
state.routes = data;
}
},
//异步
actions: {}
})
封装菜单请求工具类
在utils/menus.js编写
import { getRequest } from "./api";
export const initMenu = (router, store) => { //初始化菜单
if (store.state.routes.length > 0) { //判断菜单是否存在,如果存在就返回。
return;
}
getRequest('/system/config/menu').then(data => {
if (data) { //判断是否拿到数据,
let fmtRoutes = formatRoutes(data); //如果存在,格式化router.
router.addRoutes(fmtRoutes); //将格式和的路由添加到路由里面
//将数据存入vuex
store.commit('initRoutes', fmtRoutes);
}
})
}
export const formatRoutes = (routes) => {
let fmtRoutes = []; //格式化路由的数组。
routes.forEach(router => {
let {
path,
component,
name,
iconCls,
children,
} = router;
if (children && children instanceof Array) { //判断存在children,并且是一个数组。
//递归
children = formatRoutes(children);
}
let fmRouter = {
path: path, //路径
name: name, //名称
iconCls: iconCls, //图标
children: children,
component(resolve) {
require(['../views/' + component + '.vue'], resolve);
}
}
fmtRoutes.push(fmRouter) //将格式化的fmRouter放到fmtRoutes中
});
return fmtRoutes;
}
解决用户在页面刷新,导致vuex数据丢失
在main.js编写全局路由守卫
//全局路由前置守卫
router.beforeEach((to, from, next) => { //to跳转的路由;from:当前路由;next:调用方法,resolve钩子,实现真正的跳转
if (window.sessionStorage.getItem('tokenStr')) { //判断sessionStorage是否有tokenStr,有说明用户已经登录
initMenu(router, store); //登录的话,初始化菜单。
next();
} else {
if (to.path == '/') {
//判断是否去的登录页, 如果是就next
next();
}
}
})
在home主页面通过计算属性在vuex拿到routes
computed:{
routes(){
return this.$store.state.routes();
}
},
直接in routes就可以了
vuex报错,版本过高,不匹配
解决办法:
先卸载Vue4.xx
npm uninstall vuex --save
在安装3.xx
npm install vuex@3 --save
安装图标
cnpm install font-awesome
在main.js导入图标
import 'font-awesome/css/font-awesome.css'
在public/index.html修改样式,将margin 和padding设置为0.去掉默认边框
用户信息渲染
main.js继续判断用户信息是否存在,不存在进行请求,然后在主页面Home,拿到用户信息,进行头像渲染。
退出登录
在el-dropdown组件里面 添加@command=“commandHandler”事件,在el-dropdown-item 组件绑定
command="logout"。
在methods里面编写js,进行注销操作
commandHandler(command) {
if (command == "logout") { //判断传进的command是什么,如果是logout就进行退出操作。
this.$confirm("此操作将注销登录, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//注销登录
this.postRequest("logout");
//清空token,用户信息,菜单
window.sessionStorage.removeItem("tokenStr");
window.sessionStorage.removeItem("user");
this.$router.commit('initRoutes',[])
//跳转登录页
this.$router.replace("/");
})
.catch(() => {
this.$message({
type: "info",
message: "已取消操作",
});
});
}
},
首页面包屑
解决用户直接复制页面地址,出现空白页的问题,
如果用户没有登录,让他先去登录页面,进行登录之后,直接跳转,刚刚访问的路径页面。
main.js全局路由守卫
login登录页。
删除index.js Home路径,登录跳转home页面时报错
vue-router.esm.js?8c4f:2065 Uncaught (in promise) Error: Navigation cancelled from "/" to "/home" with a new navigation.
解决办法:
选项卡和面包屑太近解决办法:
在home.vue里面的router-view 设置样式,margin-top:10px
在components建文件夹,sys/basic/ 然后创建对应的页面,作为组件,只需要在SysBasic页面引入组件,并且注册组件,然后使用组件即可。
引入组件
注册组件
使用组件
键盘enter回车事件: @keydown.enter.native="addPosition" el-input组件生效
解决表单未确定修改,或者直接赋值表单同步覆盖问题
使用拷贝
Object.assign(this.updatePos,data);
posMana.vue
<template>
<div id="posMana">
<div>
<el-input
size="small"
class="addPosInput"
placeholder="添加职位"
suffix-icon="el-icon-plus"
@keydown.enter.native="addPosition"
v-model="pos.name"
>
</el-input>
<el-button
size="small"
icon="el-icon-plus"
type="primary"
@click="addPosition"
>添加</el-button
>
</div>
<div class="posManaMain">
<el-table
size="small"
stripe
border
:data="positions"
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="id" label="编号" width="180"> </el-table-column>
<el-table-column prop="name" label="职位" width="180">
</el-table-column>
<el-table-column prop="createDate" label="创建时间"> </el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
size="mini"
@click="showEditView(scope.$index, scope.row)"
>编辑</el-button
>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
<el-button
size="samll"
style="margin-top: 8px"
type="danger"
:disabled="multipleSelection.length==0"
@click="deleteMany"
>批量删除</el-button>
<el-dialog title="编辑职位" :visible.sync="dialogVisible" width="35%">
<div>
<el-tag>职位名称</el-tag>
<el-input
v-model="updatePos.name"
size="small"
class="updatePosInput"
></el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取 消</el-button>
<el-button size="small" type="primary" @click="doUpdate"
>确 定</el-button
>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "posMana",
data() {
return {
pos: {
name: "",
},
positions: [],
dialogVisible: false,
updatePos: {
name: "",
},
multipleSelection: []
};
},
mounted() {
this.initPositions();
},
methods: {
deleteMany() {
this.$confirm(
"此操作将永久删除[" + this.multipleSelection.length + "]条职位, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
let ids = '?';
this.multipleSelection.forEach(item=>{
ids+= 'ids='+item.id+ '&';
})
this.deleteRequest("/system/basic/pos/" + ids).then((resp) => {
if (resp) {
this.initPositions();
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
handleSelectionChange(val){
console.log(val);
this.multipleSelection = val;
},
doUpdate() {
this.putRequest("/system/basic/pos/", this.updatePos).then((resp) => {
if (resp) {
this.initPositions();
this.dialogVisible = false;
}
});
},
addPosition() {
if (this.pos.name) {
this.postRequest("/system/basic/pos/", this.pos).then((resp) => {
if (resp) {
this.initPositions();
this.pos.name = ""; //添加完将输入框内容变为空。
}
});
} else {
this.$message.error("职位名称不能为空!");
}
},
showEditView(index, data) {
// this.updatePos = data; //直接赋值会表单会同步覆盖
Object.assign(this.updatePos, data); //使用拷贝
this.updatePos.createDate = ""; //创建时间为空,不然后端错误
this.dialogVisible = true;
},
handleDelete(index, data) {
//data对应每一条json的数据
this.$confirm(
"此操作将永久删除[" + data.name + "]职位, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
this.deleteRequest("/system/basic/pos/" + data.id).then((resp) => {
if (resp) {
this.initPositions();
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
initPositions() {
this.getRequest("/system/basic/pos/").then((resp) => {
if (resp) console.log(resp);
this.positions = resp;
});
},
},
};
</script>
<style scoped>
.addPosInput {
width: 200px;
margin-right: 10px;
}
.posManaMain {
margin-top: 10px;
}
.updatePosInput {
width: 200px;
margin-left: 10px;
}
</style>
批量删除
在el-table绑定@selection-change="handleSelectionChange"方法,在return定义multipleSelection: [],在methods编写handleSelectionChange方法的事件,
handleSelectionChange(val){
console.log(val); //拿到选择行的所有信息。
this.multipleSelection = val;
},
批量删除html
<el-button
size="samll"
style="margin-top: 8px"
type="danger"
:disabled="multipleSelection.length==0" 当未选择时,禁用批量删除。
@click="deleteMany" 绑定批量删除方法
>批量删除</el-button>
批量删除方法事件
deleteMany() {
this.$confirm(
"此操作将永久删除[" + this.multipleSelection.length + "]条职位, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
let ids = '?'; //?传参
this.multipleSelection.forEach(item=>{
ids+= 'ids='+item.id+ '&'; //拼接
})
this.deleteRequest("/system/basic/pos/" + ids).then((resp) => {
if (resp) {
this.initPositions();
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
权限组页面设计
<template>
<div id="PermissMana">
<div class="permissManaTool">
<el-input size="small" placeholder="请输入角色英文名" v-model="role.name">
<template slot="prepend">ROLE_</template>
</el-input>
<el-input
size="small"
v-model="role.nameZh"
placeholder="请输入角色中文名"
></el-input>
<el-button size="small" type="primary" icon="el=icon-plus" @click="doAddRole"
>添加角色</el-button
>
</div>
<div class="permissManaMain">
<el-collapse v-model="activeName" accordion @change="change">
<el-collapse-item
:title="r.nameZh"
:name="r.id"
v-for="(r, index) in roles"
:key="index"
>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>可访问资源</span>
<el-button
style="float: right; padding: 3px 0; color: #ff0000"
type="text"
icon="el-icon-delete"
@click="doDeleteRole(r)"
></el-button>
</div>
<div>
<!-- default-checked-keys:默认选中的节点。
需要注意的是,此时必须设置node-key,其值为节点数据中的一个字段名,该字段在整棵树中是唯一的,id就是唯一的,满足-->
<el-tree
show-checkbox
:data="allMenus"
:props="defaultProps"
ref="tree"
:default-checked-keys="selectedMenus"
node-key="id"
></el-tree>
<div style="display:flex;justify-content:flex-end">
<el-button size="mini" @click="cancelUpdate">取消修改</el-button>
<el-button size="mini" type="primary" @click="doUpdate(r.id,index)">确认修改</el-button>
</div>
</div>
</el-card>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script>
export default {
name: "PermissMana",
data() {
return {
role: {
name: '',
nameZh: '',
},
roles: [],
allMenus:[],
defaultProps: {
children: "children", //allMenus对应的属性,子菜单
label: "name", //allMenus对应的属性菜单的组件名
},
selectedMenus:[],
activeName: -1 //默认-1,是关闭折叠板。
};
},
mounted() {
this.initRoles();
},
methods: {
doDeleteRole(role){
this.$confirm(
"此操作将永久删除[" + role.nameZh + "]角色, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
this.deleteRequest("/system/basic/permiss/role/" + role.id).then(
(resp) => {
if (resp) {
this.initRoles();
}
}
);
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
cancelUpdate(){
this.activeName = -1;
},
doUpdate(rid,index){
let tree = this.$refs.tree[index];
let selectedKeys = tree.getCheckedKeys(true); //true,就不会打印根节点,只会打印叶子节点,getCheckedKeys拿到选到的节点。
console.log(selectedKeys);
let url = '/system/basic/permiss/?rid=' + rid;
selectedKeys.forEach(key =>{
url += '&mids=' + key;
});
this.putRequest(url).then(resp=>{
if(resp){
this.initRoles();
this.activeName = -1; //关闭折叠面板。
}
})
},
doAddRole(){
if(this.role.name && this.role.nameZh){
console.log("0000000000");
this.postRequest('/system/basic/permiss/role',this.role).then(resp=>{
if(resp){
this.initRoles();
this.role.name = '';
this.role.nameZh = '';
}
})
}else{
// this.message.error('所有字段不能为空!');
}
},
cancelUpdate(){
this.activeName = -1;
},
change(rid){ //change传的值是el-collapse-item组件的name属性,name绑定的是r.id,所以拿到的是id值
if(rid){ //判断如果拿到id就刷新菜单,根据id值来加载获取的菜单
this.initAllMenus();
this.initSelectedMenus(rid);
}
},
initSelectedMenus(rid){
this.getRequest('/system/basic/permiss/mid/'+rid).then(resp=>{
if(resp){
this.selectedMenus = resp;
}
})
},
initAllMenus(){
this.getRequest('/system/basic/permiss/menus').then(resp=>{
if(resp){
this.allMenus = resp;
}
})
},
initRoles() {
this.getRequest("/system/basic/permiss/").then((resp) => {
if (resp) {
this.roles = resp;
}
});
},
},
};
</script>
<style scoped>
.permissManaTool {
display: flex;
justify-content: flex-start;
}
.permissManaTool .el-input {
width: 350px;
margin-right: 8px;
}
.permissManaMain {
margin-top: 10px;
width: 700px;
}
</style>
部门管理
<template>
<div style="width: 500px">
<el-input
placeholder="请输入部门名称进行搜索..."
prefix-icon="el-icon-search"
v-model="filterText"
>
</el-input>
<el-tree
style="margin-top: 10px"
:data="deps"
:props="defaultProps"
:filter-node-method="filterNode"
:expand-on-click-node="false"
ref="tree"
>
<span
class="custom-tree-node"
slot-scope="{ node, data }"
style="display: flex; justify-content: space-between; width: 100%"
>
<span>{{ data.name }}</span>
<span>
<el-button
class="depBtn"
type="primary"
size="mini"
@click="() => showAddDep(data)"
>
添加部门
</el-button>
<el-button
class="depBtn"
type="danger"
size="mini"
@click="() => deleteDep(data)"
>
删除部门
</el-button>
</span>
</span>
</el-tree>
<el-dialog title="添加部门" :visible.sync="dialogVisible" width="30%">
<div>
<table>
<tr>
<td>
<el-tag>上级部门</el-tag>
</td>
<td>{{ pname }}</td>
</tr>
<tr>
<td>
<el-tag>部门名称</el-tag>
</td>
<td>
<el-input
v-model="dep.name"
placeholder="请输入部门名称..."
></el-input>
</td>
</tr>
</table>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAddDep">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "DepMana",
data() {
return {
filterText: "",
deps: [],
defaultProps: {
children: "children",
label: "name",
},
dialogVisible: false,
dep: {
name: "",
parentId: -1,
},
pname: "",
};
},
watch: {
filterText(val) {
this.$refs.tree.filter(val);
},
},
mounted() {
this.initDeps();
},
methods: {
initDep() {
this.dep = {
name: "",
parentId: -1,
};
this.pname = "";
},
addDep2Deps(deps, dep) {
for (let i = 0; i < deps.length; i++) {
let d = deps[i]; //父部门
if (d.id == dep.parentId) {
d.children = d.children.concat(dep);
if(d.children.length > 0){
d.isParent = true;
}
return;
} else {
this.addDep2Deps(d.children, dep);
}
}
},
doAddDep() {
this.postRequest("/system/basic/department/", this.dep).then((resp) => {
if (resp) {
this.addDep2Deps(this.deps, resp.obj);
this.dialogVisible = false;
this.initDep();
}
});
},
showAddDep(data) {
console.log(data);
this.dep.parentId = data.id;
this.pname = data.name;
this.dialogVisible = true;
},
removeDepFromDeps(p,deps,id){
for(let i = 0; i < deps.length; i++){
let d = deps[i];
if(d.id == id){
deps.splice(i, 1);
if(deps.length==0){
p.isParent = false;
}
return;
}else{
this.removeDepFromDeps(d,d.children,id);
}
}
},
deleteDep(data) {
console.log(data);
if (data.isParent) {
this.$message.error("父部门删除失败!");
} else {
this.$confirm(
"此操作将永久删除[" + data.name + "]部门, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
this.deleteRequest("/system/basic/department/" + data.id).then(
(resp) => {
if (resp) {
this.removeDepFromDeps(null,this.deps, data.id);
}
}
);
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
}
},
initDeps() {
this.getRequest("system/basic/department/").then((resp) => {
if (resp) {
this.deps = resp;
}
});
},
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
},
};
</script>
<style scoped>
.depBtn {
padding: 2px;
}
</style>
操作员
问题:绑定enter事件没反应
操作员管理
<template>
<div>
<div style="display: flex; justify-content: center; margin-top: 10px">
<el-input
v-model="keywords"
placeholder="通过用户名搜索用户..."
prefix-icon="el-icon-search"
style="width: 400px; margin-right: 10px"
></el-input>
<el-button
type="primary"
icon="el-icon-search"
@click="doSearch"
@keyup.enter.native="doSearch"
>搜索</el-button
>
</div>
<div class="admin-container">
<el-card class="admin-card" v-for="(admin, index) in admins" :Key="index">
<div slot="header" class="clearfix">
<span>{{ admin.name }}</span>
<el-button
style="float: right; padding: 3px 0; color: red"
type="text"
icon="el-icon-delete"
@click="deleteAdmin(admin)"
></el-button>
</div>
<div>
<div class="img-container">
<img
:src="admin.userFace"
:alt="admin.name"
:title="admin.name"
class="userface-img"
/>
</div>
</div>
<div class="userinfo-container">
<div>用户名:{{ admin.name }}</div>
<div>手机号码:{{ admin.phone }}</div>
<div>电话号码:{{ admin.telephone }}</div>
<div>地址:{{ admin.address }}</div>
<div>
用户状态:
<el-switch
v-model="admin.enabled"
active-color="#13ce66"
inactive-color="#ff4949"
@change="enabledChange(admin)"
active-text="启用"
inactive-text="禁用"
></el-switch>
</div>
<div>
用户角色:
<el-tag
style="margin-right: 6px"
type="success"
v-for="(role, indexj) in admin.roles"
:key="indexj"
>{{ role.nameZh }}</el-tag
>
<el-popover
placement="right"
title="角色列表"
width="200"
@show="showPop(admin)"
@hide="hidePop(admin)"
>
<el-select v-model="selectedRoles" multiple placeholder="请选择">
<el-option
v-for="(r,index) in allRoles"
:key="index"
:label="r.nameZh"
:value="r.id"
>
</el-option>
</el-select>
<el-button
slot="reference"
type="text"
icon="el-icon-more"
></el-button>
</el-popover>
</div>
<div>备注:{{ admin.remark }}</div>
</div>
</el-card>
</div>
</div>
</template>
<script>
export default {
name: "SysAdmin",
data() {
return {
admins: [],
keywords: "",
allRoles:[],
selectedRoles:[]
};
},
mounted() {
this.initAdmins();
},
methods: {
hidePop(admin){
let roles = [];
Object.assign(roles,admin.roles);
let flag = false;
if(roles.length!=this.selectedRoles.length){
flag = true;
}else {
for(let i =0; i<roles.length;i++){
let role = roles[i];
for(let j =0; j < this.selectedRoles.length; j++){
let sr = this.selectedRoles[j];
if(role.id ==sr){
roles.splice(i,1);
i--;
break;
}
}
}
if(roles.length !=0){
flag = true;
}
}
if(flag){
let url = '/system/admin/role?adminId=' + admin.id;
this.selectedRoles.forEach(sr => {
url += '&rids=' +sr;
});
this.putRequest(url).then(resp =>{
if(resp){
this.initAdmins();
}
})
}
},
showPop(admin){
this.initAllRoles();
let roles = admin.roles;
this.selectedRoles = [];
roles.forEach(r => {
this.selectedRoles.push(r.id);
})
},
initAllRoles(){
this.getRequest('/system/admin/roles').then(resp=>{
if(resp){
this.allRoles = resp
}
})
},
enabledChange(admin) {
this.putRequest("system/admin/", admin).then((resp) => {
if (resp) {
this.initAdmins();
}
});
},
deleteAdmin(admin) {
this.$confirm(
"此操作将永久删除[" + admin.name + "]操作员, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
this.deleteRequest("/system/admin/" + admin.id).then((resp) => {
if (resp) {
this.initAdmins();
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
doSearch() {
this.initAdmins();
},
initAdmins() {
this.getRequest("/system/admin/?keywords=" + this.keywords).then(
(resp) => {
if (resp) {
this.admins = resp;
}
}
);
},
},
};
</script>
<style scoped>
.admin-container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
margin-top: 10px;
}
.admin-card {
width: 450px;
margin-bottom: 20px;
}
.userface-img {
width: 80px;
height: 80px;
border-radius: 80px;
}
.img-container {
width: 100%;
display: flex;
justify-content: center;
}
.userinfo-container {
font-size: 12px;
color: rgb(241, 136, 37);
}
</style>
员工资料
遇到问题:
1.Cannot read properties of undefined (reading 'getItem')"
window大写了
2."SyntaxError: Unexpected token u in JSON at position 0"
判空写法错误
错误写法: if (!window.sessionStorage.getItem("nations"))
正确写法: if (window.sessionStorage.getItem("nations")==null)
员工资料页面
<template>
<div>
<div>
<div style="display: flex; justify-content: space-between">
<div>
<el-input
style="width: 300px; margin-right: 10px"
prefix-icon="el-icon-search"
v-model="empName"
@keydown.enter.native="initEmps"
clearable
@clear="initEmps"
:disabled="showAdvanceSearchVisible"
placeholder="请输入员工名进行搜索..."
></el-input>
<el-button type="primary" icon="el-icon-search" @click="initEmps" :disabled="showAdvanceSearchVisible"
>搜索</el-button
>
<el-button type="primary" @click="showAdvanceSearchVisible = !showAdvanceSearchVisible">
<i :class="showAdvanceSearchVisible?'fa fa-angle-double-up':'fa fa-angle-double-down'" aria-hidden="true">高级搜索</i>
</el-button>
</div>
<div>
<el-upload
style="display: inline-flex; margin-right: 8px"
:headers="headers"
:show-file-list="false"
:before-upload="beforeUpload"
:on-success="onSuccess"
:on-error="onError"
:disabled="importDataDisabled"
action="/employee/basic/import"
>
<el-button
type="success"
:icon="importDataBtnIcon"
:disabled="importDataDisabled"
>
{{ importDataBtnText }}
</el-button>
</el-upload>
<el-button @click="exportData" type="success" icon="el-icon-download">
导出数据
</el-button>
<el-button type="primary" icon="el-icon-plus" @click="showAddEmpView"
>添加员工</el-button
>
</div>
</div>
</div>
<transition name="slide-fade">
<div
v-show="showAdvanceSearchVisible"
style="
border: 1px solid #409eff;
border-radius: 5px;
box-sizing: border-box;
padding: 5px;
margin: 10px 0px;
"
>
<el-row>
<el-col :span="5">
政治面貌
<el-select
v-model="searchValue.politicId"
size="mini"
style="width: 130px"
placeholder="政治面貌"
>
<el-option
v-for="item in politicsstatus"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
</el-col>
<el-col :span="4">
民族:
<el-select
v-model="searchValue.nationId"
size="mini"
style="width: 130px"
placeholder="民族"
>
<el-option
v-for="item in nations"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
</el-col>
<el-col :span="4">
职位:
<el-select
v-model="searchValue.posId"
size="mini"
style="width: 130px"
placeholder="职位"
>
<el-option
v-for="item in positions"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
</el-col>
<el-col :span="4">
职称:
<el-select
v-model="searchValue.jobLevelId"
size="mini"
style="width: 130px"
placeholder="职称"
>
<el-option
v-for="item in joblevels"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
</el-col>
<el-col :span="7">
聘用形式:
<el-radio-group v-model="searchValue.engageForm" style="margin-top: 5px">
<el-radio label="劳动合同">劳动合同</el-radio>
<el-radio label="劳务合同">劳务合同</el-radio>
</el-radio-group>
</el-col>
</el-row>
<el-row style="margin-top: 10px">
<el-col :span="5">
所属部门
<el-popover
placement="bottom"
title="请选择部门"
width="150px"
trigger="manual"
v-model="visible2"
>
<el-tree
:data="allDeps"
:props="defaultProps"
default-expand-all
@node-click="searchHandleNodeClick"
></el-tree>
<div
slot="reference"
style="
width: 130px;
display: inline-flex;
border: 1px solid #dedede;
height: 24px;
border-radius: 3px;
cursor: pointer;
align-items: center;
font-size: 13px;
padding-left: 8px;
box-sizing: border-box;
"
@click="showDepView2"
>
{{ inputDepName }}
</div>
</el-popover>
</el-col>
<el-col :span="10">
入职日期:
<el-date-picker
v-model="searchValue.beginDateScope"
size="mini"
type="daterange"
value-format="yyyy-MM-dd"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
>
</el-date-picker>
</el-col>
<el-col :span="5" :offset="4">
<el-button size="mini">取消</el-button>
<el-button @click="initEmps('advanced')" size="mini" icon="el-icon-search" type="primary"
>搜索</el-button
>
</el-col>
</el-row>
</div>
</transition>
<div style="margin-top: 10px">
<el-table
:data="emps"
stripe
border
v-loading="loading"
element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
style="width: 100%"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="name" label="姓名" width="80" fixed>
</el-table-column>
<el-table-column prop="workID" label="工号" align="left" width="90">
</el-table-column>
<el-table-column prop="gender" label="性别" align="left" width="50">
</el-table-column>
<el-table-column
prop="birthday"
label="出生日期"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="idCard"
label="身份证号码"
align="left"
width="160"
>
</el-table-column>
<el-table-column
prop="wedlock"
label="婚姻状况"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="nation.name"
label="民族"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="nativePlace"
label="籍贯"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="politicsStatus.name"
label="政治面貌"
align="left"
width="100"
>
</el-table-column>
<el-table-column prop="email" label="电子邮件" align="left" width="190">
</el-table-column>
<el-table-column prop="phone" label="电话号码" align="left" width="110">
</el-table-column>
<el-table-column
prop="address"
label="联系地址"
align="left"
width="270"
>
</el-table-column>
<el-table-column
prop="department.name"
label="所属部门"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="joblevel.name"
label="职称"
align="left"
width="120"
>
</el-table-column>
<el-table-column
prop="position.name"
label="职位"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="engageForm"
label="聘用形式"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="tiptopDegree"
label="最高学历"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="school"
label="毕业院校"
align="left"
width="150"
>
</el-table-column>
<el-table-column prop="specialty" label="专业" align="left" width="140">
</el-table-column>
<el-table-column
prop="workState"
label="在职状态"
align="left"
width="70"
>
</el-table-column>
<el-table-column
prop="beginDate"
label="入职日期"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="conversionTime"
label="转正日期"
align="left"
width="90"
>
</el-table-column>
<el-table-column
prop="endContract"
label="合同截止日期"
align="left"
width="110"
>
</el-table-column>
<el-table-column label="合同期限" align="left" width="90">
<template slot-scope="scope">
<el-tag>{{ scope.row.contractTerm }}</el-tag
>年
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200">
<template slot-scope="scope">
<el-button
@click="showEditEmpView(scope.row)"
style="padding: 3px"
size="mini"
>编辑</el-button
>
<el-button style="padding: 3px" size="mini" type="primary"
>查看高级资料</el-button
>
<el-button
@click="deleteEmp(scope.row)"
style="padding: 3px"
size="mini"
type="danger"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<div style="display: flex; justify-content: flex-end">
<el-pagination
background
@current-change="currentChange"
@size-change="sizeChange"
layout="sizes,prev, pager, next,jumper,->,total"
:total="total"
>
</el-pagination>
</div>
</div>
<el-dialog :title="title" :visible.sync="dialogVisible" width="80%">
<div>
<el-form ref="empForm" :model="emp" :rules="rules">
<el-row>
<el-col :span="6">
<el-form-item label="姓名:" prop="name">
<el-input
size="mini"
prefix-icon="el-icon-edit"
style="width: 150px"
v-model="emp.name"
placeholder="请输入员工姓名"
></el-input>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="性别:" prop="gender">
<el-radio-group v-model="emp.gender" style="margin-top: 10px">
<el-radio label="男">男</el-radio>
<el-radio label="女">女</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="出生日期:" prop="birthday">
<el-date-picker
v-model="emp.birthday"
type="date"
size="mini"
style="width: 160px"
value-format="yyyy-MM-dd"
placeholder="选择日期"
>
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="政治面貌:" prop="politicId">
<el-select
v-model="emp.politicId"
size="mini"
style="width: 200px"
placeholder="政治面貌"
>
<el-option
v-for="item in politicsstatus"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="民族:" prop="nationId">
<el-select
v-model="emp.nationId"
size="mini"
style="width: 150px"
placeholder="民族"
>
<el-option
v-for="item in nations"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="籍贯:" prop="nativePlace">
<el-input
v-model="emp.nativePlace"
placeholder="请输入籍贯"
prefix-icon="el-icon-edit"
size="mini"
style="width: 150px"
></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="电子邮箱:" prop="email">
<el-input
v-model="emp.email"
placeholder="请输入电子邮箱"
prefix-icon="el-icon-message"
size="mini"
style="width: 160px"
></el-input>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="联系地址:" prop="address">
<el-input
v-model="emp.address"
placeholder="请输入联系地址"
prefix-icon="el-icon-edit"
size="mini"
style="width: 200px"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="职位:" prop="posId">
<el-select
v-model="emp.posId"
size="mini"
style="width: 150px"
placeholder="职位"
>
<el-option
v-for="item in positions"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="职称:" prop="jobLevelId">
<el-select
v-model="emp.jobLevelId"
size="mini"
style="width: 150px"
placeholder="职称"
>
<el-option
v-for="item in joblevels"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所属部门:" prop="departmentId">
<el-popover
placement="bottom"
title="请选择部门"
width="200"
trigger="manual"
v-model="visible"
>
<el-tree
:data="allDeps"
:props="defaultProps"
default-expand-all
@node-click="handleNodeClick"
></el-tree>
<div
slot="reference"
style="
width: 160px;
display: inline-flex;
border: 1px solid #dedede;
height: 24px;
border-radius: 3px;
cursor: pointer;
align-items: center;
font-size: 13px;
padding-left: 8px;
box-sizing: border-box;
"
@click="showDepView"
>
{{ inputDepName }}
</div>
</el-popover>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="电话号码:" prop="phone">
<el-input
v-model="emp.phone"
placeholder="电话号码"
prefix-icon="el-icon-edit"
size="mini"
style="width: 200px"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="工号:" prop="workID">
<el-input
v-model="emp.workID"
placeholder="请输入工号"
prefix-icon="el-icon-edit"
disabled
size="mini"
style="width: 150px"
></el-input>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学历:" prop="tiptopDegree">
<el-select
v-model="emp.tiptopDegree"
size="mini"
style="width: 150px"
placeholder="学历"
>
<el-option
v-for="item in tiptopDegrees"
:key="item"
:label="item"
:value="item"
>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="毕业学校:" prop="school">
<el-input
v-model="emp.school"
placeholder="请输入毕业学校"
prefix-icon="el-icon-edit"
size="mini"
style="width: 160px"
></el-input>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="专业名称:" prop="specialty">
<el-input
v-model="emp.specialty"
placeholder="请输入专业名称"
prefix-icon="el-icon-edit"
size="mini"
style="width: 200px"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="入职日期:" prop="beginDate">
<el-date-picker
v-model="emp.beginDate"
type="date"
size="mini"
style="width: 120px"
value-format="yyyy-MM-dd"
placeholder="入职日期"
>
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="转正日期:" prop="conversionTime">
<el-date-picker
v-model="emp.conversionTime"
type="date"
size="mini"
style="width: 122px"
value-format="yyyy-MM-dd"
placeholder="转正日期"
>
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="合同起始日期:" prop="beginContract">
<el-date-picker
v-model="emp.beginContract"
type="date"
size="mini"
style="width: 133px"
value-format="yyyy-MM-dd"
placeholder="合同起始日期"
>
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="合同截止日期:" prop="endContract">
<el-date-picker
v-model="emp.endContract"
type="date"
size="mini"
style="width: 170px"
value-format="yyyy-MM-dd"
placeholder="合同截止日期"
>
</el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="身份证号码:" prop="idCard">
<el-input
v-model="emp.idCard"
placeholder="请输入身份证号码"
prefix-icon="el-icon-edit"
size="mini"
style="width: 180px"
></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="聘用形式:" prop="engageForm">
<el-radio-group
v-model="emp.engageForm"
style="margin-top: 10px"
>
<el-radio label="劳动合同">劳动合同</el-radio>
<el-radio label="劳务合同">劳务合同</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="婚姻状况:" prop="wedlock">
<el-radio-group v-model="emp.wedlock" style="margin-top: 10px">
<el-radio label="已婚">已婚</el-radio>
<el-radio label="未婚">未婚</el-radio>
<el-radio label="离异">离异</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAddEmp">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "EmpBasic",
data() {
return {
searchValue:{
politicId:null,
nationId:null,
posId:null,
jobLevelId:null,
engageForm:'',
departmentId:null,
beginDateScope:null
},
showAdvanceSearchVisible:false,
headers: {
Authorization: window.sessionStorage.getItem("tokenStr"),
},
importDataDisabled: false,
importDataBtnText: "导入数据",
importDataBtnIcon: "el-icon-upload2",
title: "",
defaultProps: {
children: "children",
label: "name",
},
//部门回显数据
inputDepName: "",
allDeps: [],
visible: false,
visible2:false,
emps: [],
loading: false,
total: 0,
currentPage: 1,
size: 10,
empName: "",
dialogVisible: false,
nations: [],
joblevels: [],
politicsstatus: [],
positions: [],
tiptopDegrees: [
"博士",
"硕士",
"本科",
"大专",
"高中",
"初中",
"小学",
"其他",
],
emp: {
id: null,
name: "",
gender: "",
birthday: "",
idCard: "",
wedlock: "",
nationId: null,
nativePlace: "",
politicId: null,
email: "",
phone: "",
address: "",
departmentId: null,
jobLevelId: null,
posId: null,
engageForm: "",
tiptopDegree: "",
specialty: "",
school: "",
beginDate: "",
workState: "在职",
workID: "",
contractTerm: null,
conversionTime: "",
notWorkDate: null,
beginContract: "",
endContract: "",
workAge: null,
salaryId: null,
},
rules: {
name: [{ required: true, message: "请输入员工姓名", trigger: "blur" }],
gender: [
{ required: true, message: "请选择员工性别", trigger: "blur" },
],
birthday: [
{ required: true, message: "请输入出生日期", trigger: "blur" },
],
idCard: [
{ required: true, message: "请输入身份证号码", trigger: "blur" },
{
patten:
/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
message: "身份证号码不正确",
trigger: "blur",
},
],
wedlock: [
{ required: true, message: "请选择婚姻状况", trigger: "blur" },
],
nationId: [{ required: true, message: "请输入民族", trigger: "blur" }],
nativePlace: [
{ required: true, message: "请输入籍贯", trigger: "blur" },
],
politicId: [
{ required: true, message: "请输入政治面貌", trigger: "blur" },
],
email: [
{ required: true, message: "请输入邮箱地址", trigger: "blur" },
{ type: "email", message: "邮箱地址格式不正确", tigger: "blur" },
],
phone: [{ required: true, message: "请输入电话号码", trigger: "blur" }],
address: [
{ required: true, message: "请输入员工地址", trigger: "blur" },
],
departmentId: [
{ required: true, message: "请输入部门名称", trigger: "blur" },
],
jobLevelId: [
{ required: true, message: "请输入职称", trigger: "blur" },
],
posId: [{ required: true, message: "请输入职位", trigger: "blur" }],
engageForm: [
{ required: true, message: "请选择聘用形式", trigger: "blur" },
],
tiptopDegree: [
{ required: true, message: "请输入学历", trigger: "blur" },
],
specialty: [{ required: true, message: "请输入专业", trigger: "blur" }],
school: [
{ required: true, message: "请输入毕业院校", trigger: "blur" },
],
beginDate: [
{ required: true, message: "请输入入职日期", trigger: "blur" },
],
workState: [
{ required: true, message: "请输入工作状态", trigger: "blur" },
],
workID: [{ required: true, message: "请输入工号", trigger: "blur" }],
contractTerm: [
{ required: true, message: "请输入合同期限", trigger: "blur" },
],
conversionTime: [
{ required: true, message: "请输入转正日期", trigger: "blur" },
],
notWorkDate: [
{ required: true, message: "请输入离职日期", trigger: "blur" },
],
beginContract: [
{ required: true, message: "请输入合同起始日期", trigger: "blur" },
],
endContract: [
{ required: true, message: "请输入合同结束日期", trigger: "blur" },
],
workAge: [{ required: true, message: "请输入工龄", trigger: "blur" }],
},
};
},
mounted() {
//为了让搜索时的职位下拉框有数据
this.initPositions();
this.initEmps();
this.initData();
},
methods: {
onSuccess() {
this.importDataBtnIcon = "el-icon-upload2";
this.importDataBtnText = "导入数据";
this.importDataDisabled = true;
this.initEmps();
},
onError() {
this.importDataBtnIcon = "el-icon-upload2";
this.importDataBtnText = "导入数据";
this.importDataDisabled = true;
},
beforeUpload() {
this.importDataBtnIcon = "el-icon-loading";
this.importDataBtnText = "正在导入";
//在导入数据的时候,禁用导入,不可再次点击导入文件。
this.importDataDisabled = true;
},
exportData() {
this.downloadRequest("/employee/basic/export");
},
showEditEmpView(data) {
this.title = "编辑员工信息";
this.emp = data;
//将自定义的数据赋值上去
this.inputDepName = data.department.name;
console.log(data.department);
//处理职位显示问题,职位没有存在session,在添加窗口时,掉接口,这里同样要处理
this.initPositions();
this.dialogVisible = true;
},
deleteEmp(data) {
this.$confirm("此操作将永久删除" + data.name + ", 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.deleteRequest("/employee/basic/" + data.id).then((resp) => {
if (resp) {
this.initEmps();
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
doAddEmp() {
//判断id是否为空,添加员工id为null,编辑是有id的。
if (this.emp.id) {
this.$refs["empForm"].validate((valid) => {
if (valid) {
this.putRequest("/employee/basic/", this.emp).then((resp) => {
if (resp) {
this.dialogVisible = false;
this.initEmps();
}
});
} else {
this.$message.error("添加失败!");
}
});
} else {
this.$refs["empForm"].validate((valid) => {
if (valid) {
this.postRequest("/employee/basic/", this.emp).then((resp) => {
if (resp) {
this.dialogVisible = false;
this.initEmps();
}
});
} else {
this.$message.error("添加失败!");
}
});
}
},
searchHandleNodeClick(data){
this.inputDepName = data.name;
this.searchValue.departmentId = data.id;
this.visible2 = !this.visible2;
},
handleNodeClick(data) {
this.inputDepName = data.name;
this.emp.departmentId = data.id;
this.visible = !this.visible;
},
showDepView2(){
this.visible2 = !this.visible2;
},
showDepView() {
this.visible = !this.visible;
},
//展示对话框的时候调用
getMaxWorkID() {
this.getRequest("/employee/basic/maxWorkID").then((resp) => {
if (resp) {
this.emp.workID = resp.obj;
}
});
},
//对于会变动的数据,在打开对话框中调用
initPositions() {
this.getRequest("/employee/basic/positions").then((resp) => {
if (resp) this.positions = resp;
});
},
//对于不怎么会变动的数据放在sessionStorage里面
initData() {
//判断有没有在sessionStorage里面,如果在直接查,没有就调用接口去查
if (window.sessionStorage.getItem("nations") == null) {
this.getRequest("/employee/basic/nations").then((resp) => {
if (resp) {
this.nations = resp;
window.sessionStorage.setItem("nations", JSON.stringify(resp));
}
});
} else {
this.nations = JSON.parse(window.sessionStorage.getItem("nations"));
}
if (window.sessionStorage.getItem("joblevels") == null) {
this.getRequest("/employee/basic/joblevels").then((resp) => {
if (resp) {
this.joblevels = resp;
window.sessionStorage.setItem("joblevels", JSON.stringify(resp));
}
});
} else {
this.joblevels = JSON.parse(window.sessionStorage.getItem("joblevels"));
}
if (window.sessionStorage.getItem("politicsstatus") == null) {
this.getRequest("/employee/basic/politicsstatus").then((resp) => {
if (resp) {
this.politicsstatus = resp;
window.sessionStorage.setItem(
"politicsstatus",
JSON.stringify(resp)
);
}
});
} else {
this.politicsstatus = JSON.parse(
window.sessionStorage.getItem("politicsstatus")
);
}
if (window.sessionStorage.getItem("allDeps") == null) {
this.getRequest("/employee/basic/deps").then((resp) => {
if (resp) {
this.allDeps = resp;
window.sessionStorage.setItem("allDeps", JSON.stringify(resp));
}
});
} else {
this.allDeps = JSON.parse(window.sessionStorage.getItem("allDeps"));
}
},
sizeChange(size) {
this.size = size;
this.initEmps();
},
currentChange(currentPage) {
this.currentPage = currentPage;
this.initEmps();
},
showAddEmpView() {
this.title = "添加员工";
//将添加完之后的菜单数据清空
this.emp = {
id: null,
name: "",
gender: "",
birthday: "",
idCard: "",
wedlock: "",
nationId: null,
nativePlace: "",
politicId: null,
email: "",
phone: "",
address: "",
departmentId: null,
jobLevelId: null,
posId: null,
engageForm: "",
tiptopDegree: "",
specialty: "",
school: "",
beginDate: "",
workState: "在职",
workID: "",
contractTerm: null,
conversionTime: "",
notWorkDate: null,
beginContract: "",
endContract: "",
workAge: null,
salaryId: null,
};
this.inputDepName = "";
this.getMaxWorkID();
this.initPositions();
this.dialogVisible = true;
},
initEmps(type) {
this.loading = true;
let url = '/employee/basic/?currentPage=' + this.currentPage + '&size=' +this.size;
if(type && type == 'advanced'){
if(this.searchValue.politicId){
url += '&politicId=' + this.searchValue.politicId;
}
if(this.searchValue.nationId){
url += '&nationId=' + this.searchValue.nationId;
}
if(this.searchValue.posId){
url += '&posId=' + this.searchValue.posId;
}
if(this.searchValue.jobLevelId){
url += '&jobLevelId=' + this.searchValue.jobLevelId;
}
if(this.searchValue.engageForm){
url += '&engageForm=' + this.searchValue.engageForm;
}
if(this.searchValue.departmentId){
url += '&departmentId=' + this.searchValue.departmentId;
}
if(this.searchValue.beginDateScope){
url += '&beginDateScope=' + this.searchValue.beginDateScope;
}
}else{
url+= '&name=' + this.empName;
}
this.getRequest(url).then((resp) => {
this.loading = false;
if (resp) {
this.emps = resp.data;
this.total = resp.total;
}
});
},
},
};
</script>
<style>
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.slide-fade-enter-active {
transition: all .8s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}
</style>
导出文件
安装插件:cnpm install js-file-download –save
工资账套
<template>
<div id="name">
<div style="display: flex; justify-content: space-between">
<el-button type="primary" icon="el-icon-plus" @click="showAddSalaryView">添加工资账套</el-button>
<el-button type="success" icon="el-icon-refresh" @click="initSalaries"></el-button>
</div>
<div style="margin-top: 10px">
<el-table :data="salaries" border stripe>
<el-table-column type="selection" width="40px"></el-table-column>
<el-table-column
label="账套名称"
width="120px"
prop="name"
></el-table-column>
<el-table-column
label="基本工资"
width="70px"
prop="basicSalary"
></el-table-column>
<el-table-column
label="交通补助"
width="70px"
prop="trafficSalary"
></el-table-column>
<el-table-column
label="午餐补助"
width="70px"
prop="lunchSalary"
></el-table-column>
<el-table-column
label="奖金"
width="70px"
prop="bonus"
></el-table-column>
<el-table-column
label="启用时间"
width="100px"
prop="createDate"
></el-table-column>
<el-table-column label="养老金" align="center">
<el-table-column
label="比率"
prop="pensionPer"
width="70"
></el-table-column>
<el-table-column
label="基数"
prop="pensionBase"
width="70"
></el-table-column>
</el-table-column>
<el-table-column label="医疗保险" align="center">
<el-table-column
label="比率"
prop="medicalPer"
width="70"
></el-table-column>
<el-table-column
label="基数"
prop="medicalBase"
width="70"
></el-table-column>
</el-table-column>
<el-table-column label="公积金" align="center">
<el-table-column
label="比率"
prop="accumulationFundPer"
width="70"
></el-table-column>
<el-table-column
label="基数"
prop="accumulationFundBase"
width="70"
></el-table-column>
</el-table-column>
<el-table-column label="操作" fixed="right">
<template slot-scope="scope">
<el-button type="primary" @click="showEditSalaryView(scope.row)">编辑</el-button>
<el-button type="danger" @click="deleteSalary(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="50%"
>
<div style="display:flex;justify-content:space-around;align-items:center">
<el-steps direction="vertical" :active="activeItemIndex">
<el-step :title="itemName" v-for="(itemName,index) in salaryItemName" :key="index"></el-step>
</el-steps>
<el-input v-model="salary[title]" :placeholder="'请输入'+ salaryItemName[index]+'...'" v-for="(value,title,index) in salary" :key="index" v-show="activeItemIndex==index" style="width:200px"></el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="preStep">{{activeItemIndex==10?'取消':'上一步'}}</el-button>
<el-button type="primary" @click="nextStep"
>{{activeItemIndex==10?'完成':'下一步'}}</el-button
>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "SalSob",
data() {
return {
dialogTitle:'添加工资账套',
dialogVisible:false,
salaries: [],
activeItemIndex:0,
salaryItemName:[
'账套名称','基本工资','交通补助','午餐补助','奖金','养老金比率','养老金基数','医疗保险比率','医疗保险基数','公积金比率','公积金基数'
],
salary:{
name:'',
basicSalary:0,
trafficSalary:0,
lunchSalary:0,
bonus:0,
pensionPer:0,
pensionBase:0,
medicalPer:0,
medicalBase:0,
accumulationFundPer:0,
accumulationFundBase:0,
}
};
},
mounted() {
this.initSalaries();
},
methods: {
showEditSalaryView(data){
console.log("kkkk");
this.dialogTitle = '编辑工资账套';
this.activeItemIndex = 0;
this.salary.id = data.id;
this.salary.name = data.name;
this.salary.basicSalary = data.basicSalary;
this.salary.trafficSalary = data.trafficSalary;
this.salary.lunchSalary = data.lunchSalary;
this.salary.bonus = data.bonus;
this.salary.pensionPer = data.pensionPer;
this.salary.pensionBase = data.pensionBase;
this.salary.medicalPer = data.medicalPer;
this.salary.medicalBase = data.medicalBase;
this.salary.accumulationFundPer = data.accumulationFundPer;
this.salary.accumulationFundBase = data.accumulationFundBase;
this.dialogVisible = true;
},
deleteSalary(data){
this.$confirm(
"此操作将永久删除[" + data.name + "]工资账套, 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
this.deleteRequest("/salary/sob/" + data.id).then(
(resp) => {
if (resp) {
this.initSalaries();
}
}
);
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
preStep(){
if(this.activeItemIndex == 0){
return;
}else if(this.activeItemIndex ==10){
this.dialogVisible = false;
return;
}
this.activeItemIndex--;
},
nextStep(){
if(this.activeItemIndex == 10){
if(this.salary.id){
this.putRequest('/salary/sob/',this.salary).then(resp=>{
if(resp){
this.initSalaries();
this.dialogVisible = false;
}
})
}else{
this.postRequest('/salary/sob/',this.salary).then(resp=>{
if(resp){
this.initSalaries();
this.dialogVisible = false;
}
})
}
return;
}
this.activeItemIndex++;
},
showAddSalaryView(){
this.dialogTitle = '添加工资账套';
//初始化数据
this.salary={
name:'',
basicSalary:0,
trafficSalary:0,
lunchSalary:0,
bonus:0,
pensionPer:0,
pensionBase:0,
medicalPer:0,
medicalBase:0,
accumulationFundPer:0,
accumulationFundBase:0,
};
//让步骤条一开始就从零开始
this.activeItemIndex = 0;
this.dialogVisible = true;
},
initSalaries() {
this.getRequest("/salary/sob/").then((resp) => {
if (resp) {
this.salaries = resp;
}
});
},
},
};
</script>
<style lang="scss" scoped></style>
工资账套管理
<template>
<div id="name">
<el-table
:data="emps"
size="mini"
border
stripe
>
<el-table-column type="selection" align="left" width="55px">
</el-table-column>
<el-table-column label="姓名" prop="name" align="left" fixed width="120px">
</el-table-column>
<el-table-column label="工号" prop="workID" align="left" width="120px">
</el-table-column> " width="180"></el-table-column>
<el-table-column label="邮箱地址" prop="workID" align="left" width="200px">
</el-table-column>
<el-table-column label="电话号码" prop="workID" align="left" width="120px">
</el-table-column>
<el-table-column label="所属部门" prop="department.name" align="left" width="120px">
</el-table-column>
<el-table-column label="工资账套" align="center">
<template slot-scope="scope">
<el-tooltip placement="right" v-if="scope.row.salary">
<div slot="content">
<table>
<tr>
<td>基本工资</td>
<td>{{scope.row.salary.basicSalary}}</td>
</tr>
<tr>
<td>交通补助</td>
<td>{{scope.row.salary.trafficSalary}}</td>
</tr>
<tr>
<td>午餐补助</td>
<td>{{scope.row.salary.lunchSalary}}</td>
</tr>
<tr>
<td>奖金</td>
<td>{{scope.row.salary.bonus}}</td>
</tr>
<tr>
<td>养老金比率</td>
<td>{{scope.row.salary.pensionPer}}</td>
</tr>
<tr>
<td>养老金基数</td>
<td>{{scope.row.salary.pensionBase}}</td>
</tr>
<tr>
<td>医疗保险比率</td>
<td>{{scope.row.salary.medicalPer}}</td>
</tr>
<tr>
<td>医疗保险基数</td>
<td>{{scope.row.salary.medicalBase}}</td>
</tr>
<tr>
<td>公积金比率</td>
<td>{{scope.row.salary.accumulationFundPer}}</td>
</tr>
<tr>
<td>公积金基数</td>
<td>{{scope.row.salary.accumulationFundBase}}</td>
</tr>
</table>
</div>
<el-tag>{{scope.row.salary.name}}</el-tag>
</el-tooltip>
<el-tag v-else>暂未设置</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="centern">
<template slot-scope="scope">
<el-popover
placement="left"
title="编辑工资账套"
@show="showPop(scope.row.salary)"
@hide="hidePop(scope.row)"
width="200"
trigger="click">
<div>
<el-select v-model="currentSalary" size="mini" placeholder="请选择">
<el-option
v-for="item in salaries"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</div>
<el-button slot="reference" type="danger">修改工资账套</el-button>
</el-popover>
</template>
</el-table-column>
<el-table-column label="工号" prop="workID" align="left" width="120px">
</el-table-column>
<el-table-column label="工号" prop="workID" align="left" width="120px">
</el-table-column>
</el-table>
<div style="display:flex;justify-content:flex-end;margin-top:5px">
<el-pagination
background
@current-change="currentChange"
@size-change="sizeChange"
layout="sizes,prev,pager,next,jumper,->,total,slot"
:total="total"
>
</el-pagination>
</div>
</div>
</template>
<script>
export default {
name: "SalSobCfg",
data() {
return {
currentSalary:null,
salaries:[],
emps:[],
currentPage:1,
size:10,
total:0
};
},
mounted(){
this.initEmps();
this.initSalaries();
},
methods: {
hidePop(data){
if(this.currentSalary &&this.currentSalary!=data.id){
this.putRequest('/salary/sobcfg/?eid=' + data.id +'&sid=' +this.currentSalary).then(resp=>{
if(resp){
this.initEmps();
}
})
}
},
showPop(data){
if(data){
this.currentSalary = data.id;
}else{
this.currentSalary = null;
}
},
initSalaries(){
this.getRequest('/salary/sobcfg/salaries').then(resp=>{
if(resp){
this.salaries = resp;
}
})
},
currentChange(page){
this.currentPage = page;
this.initEmps();
},
sizeChange(size){
this.size = size;
this.initEmps();
},
initEmps(){
this.getRequest('/salary/sobcfg/?currentPage=' + this.currentPage + '&size=' +this.size).then(resp=>{
if(resp){
this.emps = resp.data;
this.total = resp.total;
}
})
}
}
};
</script>
<style lang="scss" scoped>
</style>
在线聊天
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?