这是我写云e办后端接口文档写的笔记
添加pom依赖
com.dong1024
yeb-generator
0.0.1-SNAPSHOT
com.dong1024
yeb
0.0.1-SNAPSHOT
UTF-8
1.8
1.8
org.springframework.boot
spring-boot-starter-web
com.baomidou
mybatis-plus-boot-starter
3.3.1.tmp
com.baomidou
mybatis-plus-generator
3.3.1.tmp
org.freemarker
freemarker
mysql
mysql-connector-java
runtime
CodeGenerator配置类
```java
package com.dong1024.generator;
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.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.FileOutConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.TemplateConfig;
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;
/**
* 执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
*
* @author dong
* @since 1.0.0
*/
public class CodeGenerator {
/**
*
* 读取控制台内容
*
*/
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");
gc.setOutputDir(projectPath + "/yeb-generator/src/main/java");
//作者
gc.setAuthor("dong");
//打开输出目录
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.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.dong1024.server")
.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 focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/yeb-generator/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();
}
}
对照数据库里的字段在控制台输入:
随后自动生成mapper,pojo,service,controller类,把生成的类跟xml文件放入yeb-server模块中
1.添加依赖
<!-- swagger2 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Swagger第三方ui依赖 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<!--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>
2.添加yaml配置文件的jwt属性
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: yeb-secret
# JWT的超期限时间(60*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer
3.jwt工具类的编写
package com.dong1024.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;
/**
* jwtToken工具类
*/
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据用户信息生成token
*
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 从token中获取登录用户名
* @param token
* @return
*/
public String getUserNameFromToken(String token){
String username;
try {
Claims claims = getClaimsFormToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否有效
* @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 = getClaimsFormToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
/**
* 判断token是否失效
* @param token
* @return
*/
private boolean isTokenExpired(String token) {
Date expireDate = getExpiredDateFromToken(token);
return expireDate.before(new Date());
}
/**
* 从token中获取过期时间
* @param token
* @return
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFormToken(token);
return claims.getExpiration();
}
/**
* 从token中获取荷载
* @param token
* @return
*/
private Claims getClaimsFormToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.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)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 生成token失效时间
*
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
}
4.公共返回对象
package com.dong1024.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@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);
}
public static RespBean error(String message){
return new RespBean(500,message,null);
}
public static RespBean error(String message,Object obj){
return new RespBean(500,message,obj);
}
}
1.创建LoginParams类
可以用Admin类,但是我们只需要通过用户名、密码、验证码这三个参数来进行登录验证,因此创建一个AdminLoginParams,里面只需用户名密码验证码三个参数即可
package com.dong1024.server.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户登录实体类
*/
@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;
}
/**
1. 登录
*/
@Api(tags = "LoginController")
@RestController
public class LoginController {
@Autowired
private IAdminService adminService;
@ApiOperation(value = "登录之后返回token")
@PostMapping("/login")
//可以用admin,但是用AdminLoginParam可以只需要接收传递的账号密码
public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),request,adminLoginParam.getCode());
}
@ApiOperation(value = "获取当前登录用户的信息")
@GetMapping("/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;
}
@ApiOperation(value = "退出登录")
@PostMapping("/logout")
public RespBean logout(){
return RespBean.success("注销成功!");
}
}
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
public RespBean login(String username, String password, HttpServletRequest request, String code) {
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String captcha = (String) request.getSession().getAttribute("captcha");
if (StringUtils.isEmpty(code) || !captcha.equalsIgnoreCase(code)) {
return RespBean.error("验证码输入错误,请重新输入!");
}
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()/*权限列表*/);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//生成token
String token = jwtTokenUtil.generateToken(userDetails);
Map<String, String> tokenMap = new HashMap<>();
//token信息
tokenMap.put("token", token);
//头部信息,放在前端请求头里
tokenMap.put("tokenHead", tokenHead);
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));
}
package com.dong1024.server.config.security;
import com.dong1024.server.config.security.component.*;
import com.dong1024.server.pojo.Admin;
import com.dong1024.server.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;
import java.nio.file.FileAlreadyExistsException;
/**
* Security配置类
*
* @author dong
* @since 1.0.0
*/
@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",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/captcha",
"/ws/**"
);
}
@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;
}
})
//动态权限配置
/* .withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilter);
return object;
}
})*/
.and()
//禁用缓存
.headers()
.cacheControl();
//添加jwt 登录授权过滤器
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
@Override
@Bean
public UserDetailsService userDetailsService(){
return username -> {
Admin admin = adminService.getAdminByUserName(username);
if (null!=admin){
admin.setRoles(adminService.getRoles(admin.getId()));
return admin;
}
throw new UsernameNotFoundException("用户名或密码不正确");
};
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){
return new JwtAuthencationTokenFilter();
}
}
2.创建JWT登录授权过滤器
package com.dong1024.server.config.security.component;
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登录授权过滤器
*
* @author dong
* @since 1.0.0
*/
public class JwtAuthencationTokenFilter 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);
}
}
package com.dong1024.server.config.security.component;
import com.dong1024.server.pojo.RespBean;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当未登录或者token失效时访问接口时,自定义的返回结果
*/
@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();
}
}
当未登录时返回结果,这个根据自己具体的业务需求定义返回结果
package com.dong1024.server.config.security.component;
import com.dong1024.server.pojo.RespBean;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当未登录时访问接口时,自定义返回结果
*/
@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();
}
}
<!-- swagger2 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Swagger第三方ui依赖 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
package com.dong1024.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
/**
* Swagger2配置类
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//包扫描
.apis(RequestHandlerSelectors.basePackage("com.dong1024.server.controller"))
.paths(PathSelectors.any())
.build()
.securityContexts(securityContexts())
.securitySchemes(securitySchemes());
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("云E办接口文档")
.description("云E办接口文档")
.contact(new Contact("dong","http:localhost:8081/doc.html","[email protected]"))
.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;
}
}
注:不要忘了在SpringSequrity配置类放行Swagger,我那时已经在配置里把所有的放行了
为了防止Spring security拦截路径,每次都需要登录才能操作接口文档,Swagger2提供了全局参数Authorize
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;
}
验证码可以用hutool,谷歌等验证码方式编写,这里我使用谷歌的方法
<!-- google kaptcha依赖 -->
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
2.验证码配置类
package com.dong1024.server.config;
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;
/**
* 验证码配置类
*
* @author zhoubin
* @since 1.0.0
*/
@Configuration
public class CaptchaConfig {
@Bean
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;
}
}
3.编写验证码的controller类
这里不需要记忆,基本在网上可以找到,中间的begin和end才是真正的代码逻辑
package com.dong1024.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;
/**
* 验证码
*
* @author zhoubin
* @since 1.0.0
*/
@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配置类里放行captcha
4.校验验证码
public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),request,adminLoginParam.getCode());
}
@Override
public RespBean login(String username, String password, HttpServletRequest request, String code) {
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String captcha = (String) request.getSession().getAttribute("captcha");
if (StringUtils.isEmpty(code) || !captcha.equalsIgnoreCase(code)) {
return RespBean.error("验证码输入错误,请重新输入!");
}
if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
return RespBean.error("用户名或密码不正确");
}
if (!userDetails.isEnabled()) {
return RespBean.error("账号被禁用,请联系管理员!");
}
1. 先在Menu实体类里添加子菜单children
@ApiModelProperty(value = "子菜单")
@TableField(exist = false)//告诉后台数据库里没有children这个字段
private List<Menu> children;
2. 编写Menucontroller类
这里可能会有人纳闷,为什么根据用户id查询菜单列表,但是却不用传递id呢?
这是因为我们登录了系统的话,后台会有你用户的所有信息,包括id,这里不需要前端传递id过来,如果依靠前端传递id,很容易被别人篡改我们的资料。
@ApiOperation(value = "通过用户id查询列表")
@GetMapping("/menu")
public List<Menu> getMenusByAdminId(){
return menuService.getMenusByAdminId();
}
3. 编写MenuService类
/**
* 根据用户id查询菜单列表
*/
@Override
public List<Menu> getMenusByAdminId() {
//从SecurityContextHolder的上下文中取到admin的id
Integer adminId = ((Admin) SecurityContextHolder.getContext().getAuthentication()
.getPrincipal()).getId();
List<Menu> menus = menuMapper.getMenusByAdminId(adminId);
}
4. 编写MenuMapper.xml文件
有两种方法,我们可以先查询所有的一级菜单,再用一级菜单查询二级菜单。第二种方法是采用自关联的方法。第一种方法需要多次查询数据库,比较耗费性能,这里采用自关联的方法,一次查询数据库即可。
<resultMap id="Menus" type="com.dong1024.server.pojo.Menu" extends="BaseResultMap">
<!--分裂菜单-->
<collection property="children" ofType="com.dong1024.server.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>
<!-- 根据用户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>
1. 添加依赖
<!-- 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>
2.在yaml配置文件中添加对应的配置
redis:
#超时时间
timeout: 10000ms
#服务器地址
host: 127.0.0.1
#服务器端口
port: 6379
#数据库
database: 0
#密码
# password: root
lettuce:
pool:
#最大连接数,默认8
max-active: 1024
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接
max-idle: 200
#最小空闲连接
min-idle: 5
3.编写redis配置类
package com.dong1024.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;
/**
* Redis配置类。
* 主要是序列化
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
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(redisConnectionFactory);
return redisTemplate;
}
}
4.在MeunService中添加RedisTemplete
/**
*
* 服务实现类
*
*
* @author dong
* @since 2021-06-18
*/
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
@Autowired
private MenuMapper menuMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据用户id查询菜单列表
*/
@Override
public List<Menu> getMenusByAdminId() {
//从SecurityContextHolder的上下文中取到admin的id
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;
}
RBAC是基于角色的访问控制( Role-Based Access Control )在RBAC中,权限与角色相关联,角色与用户相关联,因此就可以通过给予用户哪些角色来决定用户可以具有操作哪些资源的权限
根据请求的url判断角色
@ApiModelProperty(value = "角色列表")
@TableField(exist = false)
private List<Role> roles;
/**
* 根据角色获取菜单列表
* @return
*/
@Override
public List<Menu> getMenusWithRole() {
return menuMapper.getMenusWithRole();
}
**Menumapper
<resultMap id="MenusWithRole" type="com.dong1024.server.pojo.Menu" extends="BaseResultMap">
<collection property="roles" ofType="com.dong1024.server.pojo.Role">
<id column="rid" property="id"/>
<result column="rname" property="name"/>
<result column="rnameZh" property="nameZh"/>
</collection>
</resultMap>
<!-- 根据角色获取菜单列表 -->
<select id="getMenusWithRole" resultMap="MenusWithRole">
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>
package com.dong1024.server.config.security.component;
import com.dong1024.server.pojo.Menu;
import com.dong1024.server.pojo.Role;
import com.dong1024.server.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;
/**
* 权限控制
* 根据请求url分析请求所需的角色
*过滤器
* @author dong
* @since 1.0.0
*/
@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 true;
}
}
总结权限控制的核心:根据请求的url判断这个url存在于哪些角色当中,再判断登陆的用户中拥有哪些角色,因此就能匹配到用户包含有哪些可操作的url,在你访问这个url时,如果匹配到用户可操作的url就允许访问,如果匹配不到就返回一个默认的用户ROLE_LOGIN
1.在admin类里添加角色权限,在getAuthorities方法中添加拿到权限列表的方法
@ApiModelProperty(value = "角色权限")
@TableField(exist = false)
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//拿到权限的列表
List<SimpleGrantedAuthority> authorityList = roles
.stream()
//拿到权限的名字
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return authorityList;
}
2.根据用户id查询角色列表
/**
* 根据用户id查询角色列表
*
* @param adminId
* @return
*/
@Override
public List<Role> getRoles(Integer adminId) {
return roleMapper.getRoles(adminId);
}
mapper:
<select id="getRoles" resultType="com.dong1024.server.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>
在getadmininfo中设置用户权限
原本是去修改login方法,但本质上是调用了SecurityConfig配置类里的UserDetailsService方法,所以去UserDetailsService方法设置用户权限
3.配置权限控制,判断用户角色
package com.dong1024.server.config.security.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 zhoubin
* @since 1.0.0
*/
@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){
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 attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
去spring secuirty配置动态权限
配置customUrlDecisionManager和customFilter
//动态权限配置
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilter);
return object;
}
package com.dong1024.server.controller;
import com.dong1024.server.mapper.PositionMapper;
import com.dong1024.server.pojo.Position;
import com.dong1024.server.pojo.RespBean;
import com.dong1024.server.service.IPositionService;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import org.apache.ibatis.annotations.Delete;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
*
* 前端控制器
*
*
* @author dong
* @since 2021-06-18
*/
@RestController
@RequestMapping("/system/basic/pos")
public class PositionController {
@Autowired
private IPositionService positionService;
@ApiOperation(value = "查询所有职位信息")
@GetMapping("/")
public List<Position> getAllPosition(){
return positionService.list();
}
@ApiOperation(value = "添加职位信息")
@PostMapping("/")
public RespBean addPosition(@RequestBody Position position){
if (positionService.save(position)){
return RespBean.success("添加成功");
}
return RespBean.error("添加失败");
}
@ApiOperation(value = "更新职位信息")
@PutMapping("/")
public RespBean updatePosition(@RequestBody Position position){
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 deletePositionIds(Integer[] ids) {
if (positionService.removeByIds(Arrays.asList(ids))) {
return RespBean.success("删除成功");
}
return RespBean.error("删除失败");
}
}
注:@RequestMapping("/system/basic/pos")要按照数据库t_menu的url格式,否则会报权限不足
如果我们去删除某一个职位信息,这个职位如果有外键关联的话,控制台就会报错,但是用户是看不懂的,所以我们需要创建一个全局异常处理类,提醒用户错误原因
package com.dong1024.server.exception;
import com.dong1024.server.pojo.RespBean;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理
*
* @author dong
* @since 1.0.0
*/
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler(SQLException.class)
public RespBean mySqlException(SQLException e){
if (e instanceof SQLIntegrityConstraintViolationException){
return RespBean.error("该数据有关联数据,操作失败!");
}
return RespBean.error("数据库异常,操作失败!");
}
}
跟职位管理的实现方法一样,用mybatisplus方法
package com.dong1024.server.controller;
import com.dong1024.server.pojo.Joblevel;
import com.dong1024.server.pojo.RespBean;
import com.dong1024.server.service.IJoblevelService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.ibatis.annotations.Delete;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
*
* 前端控制器
*
*
* @author dong
* @since 2021-06-18
*/
@RestController
@RequestMapping("/system/basic/joblevel")
public class JoblevelController {
@Autowired
private IJoblevelService joblevelService;
@ApiOperation("获取所有职称 ")
@GetMapping("/")
public List<Joblevel> getAllJoblevel(){
return joblevelService.list();
}
@ApiOperation("添加职位")
@PostMapping("/")
public RespBean addJoblevel(@RequestBody Joblevel joblevel){
joblevel.setCreateDate(LocalDateTime.now());
if (joblevelService.save(joblevel)){
return RespBean.success("添加成功");
}
return RespBean.error("添加失败");
}
@ApiOperation("更新职称")
@PutMapping("/")
public RespBean updateJoblevel(@RequestBody Joblevel joblevel){
if (joblevelService.updateById(joblevel)){
return RespBean.success("更新成功");
}
return RespBean.error("更新失败");
}
@ApiOperation("删除职称")
@DeleteMapping("/{id}")
public RespBean deleteByIdJoblevel(@PathVariable Integer id){
if (joblevelService.removeById(id)){
return RespBean.success("删除成功");
}
return RespBean.error("删除失败");
}
@ApiOperation("批量删除职称")
@DeleteMapping("/")
public RespBean deleteByIdsJoblevel(Integer[] ids){
if (joblevelService.removeByIds(Arrays.asList(ids))){
return RespBean.success("批量删除成功");
}
return RespBean.error("批量删除失败");
}
}
1.注意添加角色需要判断是否是ROLE_开头的,如果不是的话需要给他设置ROLE_前缀
@RestController
@RequestMapping("/system/basic/permission")
public class PermissionController {
@Autowired
private IRoleService roleService;
@Autowired
private IMenuService menuService;
@Autowired
private IMenuRoleService menuRoleService;
@ApiOperation("获取所有角色")
@GetMapping("/")
public List<Role> getAllRole() {
return roleService.list();
}
@ApiOperation("添加角色")
@PostMapping("/")
public RespBean addRole(@RequestBody Role role) {
//判断role开头的名字是否是ROLE_开头的,不是的话给他添加ROLE_前缀
if (!role.getName().startsWith("ROLE_")) {
role.setName("ROLE_" + role.getName());
}
if (roleService.save(role)) {
return RespBean.success("添加成功");
}
return RespBean.error("添加失败");
}
@ApiOperation("删除角色")
@PostMapping("/role/{id}")
public RespBean deleteRole(@PathVariable Integer id) {
if (roleService.removeById(id)) {
return RespBean.success("删除成功");
}
return RespBean.error("删除失败");
}
}
2.查询所有菜单
由于菜单是有子菜单的,一共有三级菜单,所以不能用mybatispuls的list()方法
@ApiOperation("查询所有菜单")
@GetMapping("/menus")
public List<Menu> getAllMenus() {
return menuService.getAllMenus();
}
-----------------------------
<resultMap id="MenusWithChildren" type="com.dong1024.server.pojo.Menu" extends="BaseResultMap">
<id column="id1" property="id"/>
<result column="name1" property="name"/>
<collection property="children" ofType="com.dong1024.server.pojo.Menu">
<id column="id2" property="id"/>
<result column="name2" property="name"/>
<collection property="children" ofType="com.dong1024.server.pojo.Menu">
<id column="id3" property="id"/>
<result column="name3" property="name"/>
</collection>
</collection>
</resultMap>
<!--查询所有菜单-->
<select id="getAllMenus" resultMap="MenusWithChildren">
SELECT
m1.id AS id1,
m1.`name` AS name1,
m2.id AS id2,
m2.`name` AS name2,
m3.id AS id3,
m3.`name` AS name3
FROM
t_menu m1,
t_menu m2,
t_menu m3
WHERE
m1.id = m2.parentId
AND m2.id = m3.parentId
AND m3.enabled = TRUE
</select>
3.根据角色id查询菜单id
@ApiOperation("根据角色id查询菜单id")
@GetMapping("/menus/{rid}")
public List<Integer> getByIdMenus(@PathVariable Integer rid){
return menuRoleService.list(new QueryWrapper<MenuRole>().eq("rid",rid))
//用steam流转出menuRole的菜单id
.stream().map(MenuRole::getMid/*拿到menuRole的菜单id*/).collect(Collectors.toList());
}
4.更新角色的权限菜单
更新角色的权限菜单是比较复杂的,如果单纯只是增加或者删除是比较容易的。假设我们这个角色id有一个权限菜单,但是我们需要删除这个权限,再添加两个权限菜单,这时我们就要考虑这两个菜单我之前有没有权限,现在有没有权限,就比较麻烦。
所以现在我们用比较暴力的办法解决,就是直接把所有菜单清空,再插入数据。
package com.dong1024.server.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dong1024.server.mapper.MenuRoleMapper;
import com.dong1024.server.pojo.Menu;
import com.dong1024.server.pojo.MenuRole;
import com.dong1024.server.pojo.RespBean;
import com.dong1024.server.service.IMenuRoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
*
* 服务实现类
*
*
* @author dong
* @since 2021-06-18
*/
@Service
public class MenuRoleServiceImpl extends ServiceImpl<MenuRoleMapper, MenuRole> implements IMenuRoleService {
@Autowired
private MenuRoleMapper menuRoleMapper;
@Override
@Transactional
public RespBean updateMenuRole(Integer rid, Integer[] mids) {
//先根据roleId把所有菜单删除
menuRoleMapper.delete(new QueryWrapper<MenuRole>().eq("rid",rid));
//判断是否只更新了一条数据,而那个角色的权限只有一条数据,这代表把这个菜单删除了
if(null==mids||0==mids.length){
return RespBean.success("更新成功");
}
Integer num = menuRoleMapper.insertMenuRole(rid,mids);
if (num==mids.length){
return RespBean.success("更新成功");
}
return RespBean.error("更新失败");
}
}
--------------------------------------
<!-- 更新角色菜单 -->
<insert id="insertMenuRole">
insert into t_menu_role(mid,rid) values
<foreach collection="mids" item="mid" separator=",">
(#{
mid},#{
rid})
</foreach>
</insert>
addDep的函数
CREATE DEFINER=`root`@`localhost` PROCEDURE `addDep`(in depName varchar(32),in parentId int,in enabled boolean,out result int,out result2 int)
begin
declare did int;
declare pDepPath varchar(64);
insert into t_department set name=depName,parentId=parentId,enabled=enabled;
select row_count() into result;
select last_insert_id() into did;
set result2=did;
select depPath into pDepPath from t_department where id=parentId;
update t_department set depPath=concat(pDepPath,'.',did) where id=did;
update t_department set isParent=true where id=parentId;
end
deleteDep的函数
CREATE DEFINER=`root`@`localhost` PROCEDURE `deleteDep`(in did int,out result int)
begin
declare ecount int;
declare pid int;
declare pcount int;
declare a int;
select count(*) into a from t_department where id=did and isParent=false;
if a=0 then set result=-2;
else
select count(*) into ecount from t_employee where departmentId=did;
if ecount>0 then set result=-1;
else
select parentId into pid from t_department where id=did;
delete from t_department where id=did and isParent=false;
select row_count() into result;
select count(*) into pcount from t_department where parentId=pid;
if pcount=0 then update t_department set isParent=false where id=pid;
end if;
end if;
end if;
end
1.1在department实体类中添加返回结果
@ApiModelProperty("返回结果,存储过程使用")
@TableField(exist = false)
private Integer result;
既然查询结果要让子部门层层嵌套在父部门中,那么该怎么进行查询呢,可不是简单的select * from t_department了!
我们可以通过parentId = -1先查询出最外层的父部门股东会,再通过把股东会的id作为parentId查询出董事会,再通过把董事会的id为parentId查询出总办,在通过把总办为parentId查询出财务部,市场部,依次类推
/**
* 获取所有部门
* @return
*/
@Override
public List<Department> getAllDepartments() {
return departmentMapper.getAllDepartments(-1);
}
其实就是递归调用mysql查询
那么如何使用递归调用呢,可以通过resultMap进行递归调用
<resultMap id="DepartmentWithChildren" type="com.dong1024.server.pojo.Department" extends="BaseResultMap">
<collection property="children" ofType="com.dong1024.server.pojo.Department"
select="com.dong1024.server.mapper.DepartmentMapper.getAllDepartments" column="id">
</collection>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id
, name, parentId, depPath, enabled, isParent
</sql>
<!--获取所有部门-->
<select id="getAllDepartment" resultMap="departmentWithChildren">
select
<include refid="Base_Column_List"/>
from t_department
where parentId = #{
parentId}
</select>
我们在表中设了默认值,enabled默认为1,isParent默认为0
depPath就是从后往前看,最后面是自己的id,再往前是它的父id,再往前是它父id的父id,以此类推,直到指向最外层的父部门
添加时,我们只需要传入name和parentId两个参数即可实现添加,比如我要在技术部下面添加技术总监,添加逻辑为:
面对这么复杂的过程我们不妨使用存储过程
-- DEFINER=`root`@`localhost`:定义用户,使用本机的ip地址
CREATE DEFINER=`root`@`localhost` PROCEDURE `addDep`(in depName varchar(32),in
parentId int,in enabled boolean,out result int,out result2 int)
begin
-- 定义int类型变量,用来存储最后插入部门的id
declare did int;
-- 定义varchar类型变量,用来存储插入部门的父部门的deptPath
declare pDepPath varchar(64);
-- 开始向表中插入数据了
insert into t_department set name=depName,parentId=parentId,enabled=enabled;
-- 系统自带函数row_count(),为受影响行数,赋值给result,此值在操作成的前提下必维1
select row_count() into result;
-- 系统自带函数last_insert_id(),为最后插入的数据的id,赋值给did
select last_insert_id() into did;
-- result2为最后插入部门的id
set result2=did;
-- 查询插入数据的父部门的depPath,设置到pDepPath
select depPath into pDepPath from t_department where id=parentId;
-- 修改插入部门的depPath,使用拼接,用上一步父部门的depPath拼接最后插入部门的id
update t_department set depPath=concat(pDepPath,'.',did) where id=did;
-- 修改插入部门的父部门的isParent参数为1
update t_department set isParent=true where id=parentId;
end
我们在Department类中加入一个字段,因为存储过程返回结果中有result
@ApiModelProperty("返回结果,存储过程使用")
@TableField(exist = false)
private Integer result;
在service类中添加逻辑
/**
* 添加部门
* @param dep
* @return
*/
@Override
public RespBean addDep(Department dep) {
dep.setEnabled(true);
departmentMapper.addDep(dep);
if (1==dep.getResult()){
return RespBean.success("添加成功");
}
return RespBean.error("添加失败");
}
在mapper文件中添加逻辑
<!-- 添加部门 statementType是调用存储过程,也就是数据库函数-->
<select id="addDep" statementType="CALLABLE">
call addDep(#{
name,mode=IN,jdbcType=VARCHAR}
,#{
parentId,mode=IN,jdbcType=INTEGER}
,#{
enabled,mode=IN,jdbcType=BOOLEAN}
,#{
result,mode=OUT,jdbcType=INTEGER}
,#{
id,mode=OUT,jdbcType=INTEGER})
</select>
删除部门的逻辑也比较复杂,其删除逻辑为:
查询要删除的部门下面有没有子部门?
如果有,说明要删除的数据下面有关联数据直接返回结果,不能对其进行删
如果没有,继续判断
先查询是否有员工在这个部门中?
如果有,还是不能删
如果没有,继续判断
查询要删除的部门的父部门有没有子部门,也就是查询你要删除的部门有没有同级部门
如果有,什么也不用动
如果没有,设置你删除部门的父部门的isParent为0,因为它下面已经没有子部门了
所以这里也使用存储过程
-- DEFINER=`root`@`localhost`:定义用户,使用本机的ip地址
CREATE DEFINER=`root`@`localhost` PROCEDURE `deleteDep`(in did int,out result int)
begin
declare ecount int;
-- 定义一个int变量,用来接收部门的parentId
declare pid int;
declare pcount int;
-- 定义int类型变量,用来存储查询的部门个数
declare a int;
-- 通过前端传入的id查询要删除的部门且下面没有子部门,将个数赋值给a
select count(*) into a from t_department where id=did and isParent=true;
-- 如果部门数不为0,说明要删除的部门下面有子部门,不能删!设置参数中的返回值为-2
if a!=0 then
set result=-2;
else
-- 如果存在,查询与其关联的员工表中在该部门的员工数
select count(*) into ecount from t_employee where departmentId=did;
-- 如果有员工在要删除的部门,设置参数中的返回值为-1
if ecount>0 then
set result=-1;
-- 如果要删除的部门没有员工
else
-- 查询要删除部门的parentId,赋值给pid
select parentId into pid from t_department where id=did;
-- 删除指定的部门,且该部门下没有子部门
delete from t_department where id=did and isParent=false;
-- 将受影响的的行数赋值给参数的返回值
select row_count() into result;
-- 查询要删除部门的父部门还有没有子部门(查询要删除的部门还有没有同级部门)
select count(*) into pcount from t_department where parentId=pid;
if pcount=0 then
-- 如果没有子部门修改删除的部门的父部门isParent = 0
update t_department set isParent=false where id=pid;
end if;
end if;
end if;
end
service中的代码逻辑
@Override
public RespBean deleteDep(Integer id) {
Department dep = new Department();
dep.setId(id);
departmentMapper.deleteDep(dep);
if (-1==dep.getResult()){
return RespBean.error("该部门有员工存在,删除失败");
}
if (-2==dep.getResult()){
return RespBean.error("该部门有子部门存在,删除失败");
}
if (1==dep.getResult()) {
return RespBean.success("删除成功");
}
return RespBean.error("删除失败");
}
departmentMapper中的代码逻辑
<!-- 删除部门 -->
<select id="deleteDep" statementType="CALLABLE">
call deleteDep(#{
id,mode=IN,jdbcType=INTEGER},#{
result,mode=OUT,jdbcType=INTEGER})
</select>
AdminController的代码
@ApiOperation("获取所有操作员")
@GetMapping("/")
public List<Admin> getAllAdmins(String keywords){
//参数是前端传过来的关键词
return adminService.getAllAdmins(keywords);
}
AdminServiceImpl
/**
* 获取所有操作员
*
* @param keywords
* @return
*/
@Override
public List<Admin> getAllAdmins(String keywords) {
//从SecurityContextHolder的上下文中取到admin的id
return adminMapper.getAllAdmins(((Admin) SecurityContextHolder.getContext().getAuthentication()
.getPrincipal()).getId(), keywords);
}
mapper文件:
package com.dong1024.server.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dong1024.server.pojo.Admin;
import org.apache.ibatis.annotations.Param;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
/**
*
* Mapper 接口
*
*
* @author dong
* @since 2021-06-18
*/
public interface AdminMapper extends BaseMapper<Admin> {
List<Admin> getAllAdmins(@Param("id") Integer id,@Param("keywords") String keywords);
}
-------------------------------------
<resultMap id="AdminWithRole" type="com.dong1024.server.pojo.Admin" extends="BaseResultMap">
<collection property="roles" ofType="com.dong1024.server.pojo.Role">
<id column="rid" property="id" />
<result column="rname" property="name" />
<result column="rnameZh" property="nameZh" />
</collection>
</resultMap>
<!--获取所有操作员-->
<select id="getAllAdmins" resultMap="AdminWithRole">
SELECT
a.*,
r.id AS rid,
r.`name` AS rname,
r.nameZh AS rnameZh
FROM
t_admin a
LEFT JOIN t_admin_role ar ON a.id = ar.adminId
LEFT JOIN t_role r ON r.id = ar.rid
WHERE
a.id != #{
id}
<if test="null!=keywords and ''!=keywords">
AND a.`name` LIKE CONCAT( '%', #{
keywords}, '%' )
</if>
ORDER BY
a.id
</select>
更新和删除可以使用mybatisplus的内置方法
@ApiOperation("更新操作员")
@PutMapping("/")
public RespBean updateAdmins(@RequestBody Admin admin){
if (adminService.updateById(admin)){
return RespBean.success("更新成功");
}
return RespBean.error("更新失败");
}
@ApiOperation("删除操作员")
@DeleteMapping("/{id}")
public RespBean deleteAdmins(@PathVariable Integer id){
if (adminService.removeById(id)){
return RespBean.success("删除成功");
}
return RespBean.error("删除失败");
}
注:这里的更新操作员有一个坑,我们在Admin类里继承了UserDetails,并重写了isEnabled()方法,里面返回了enabled,但是原本这个类就有一个enabled的get方法,所以系统不知道要调用哪个方法,我们就用Getter注解使他不要生成get方法,用isEnabled()方法
controller层
@ApiOperation("获取所有操作员角色")
@GetMapping("/roles")
public List<Role> getAllRoles(){
return roleService.list();
}
@ApiOperation("更新操作员角色")
@PutMapping("/role")
public RespBean addAdminRole(Integer adminId, Integer[] rids){
return adminService.addAdminRole(adminId,rids);
}
service层:
/**
* 更新操作员角色
*
* @param adminId
* @param rids
* @return
*/
@Override
@Transactional
public RespBean addAdminRole(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("更新失败");
}
mapper操作:
Integer addAdminRole(Integer adminId, Integer[] rids);
--------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dong1024.server.mapper.AdminRoleMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.dong1024.server.pojo.AdminRole">
<id column="id" property="id" />
<result column="adminId" property="adminId" />
<result column="rid" property="rid" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, adminId, rid
</sql>
<!--更新操作员角色-->
<update id="addAdminRole">
insert into t_admin_role(adminId,rid) values
<foreach collection="rids" item="rid" separator=",">
(#{
adminId},#{
rid})
</foreach>
</update>
</mapper>
员工的信息可能会有成百上千条,我们不可能给它显示到一个页面中,因此需要分页,此次分页我们采用后端分页技术,使用MybatisPlus自带的分页插件
package com.dong1024.server.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* mybatisplus分页配置
*/
@Configuration
public class MyBatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
}
一般我们的分页有总页数,还有返回的数据,所以我们单独放在一个公共类里
package com.dong1024.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分页公共返回对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespPageBean {
/**
* 总条数
*/
private Long total;
/**
* 数据List
*/
private List<?> data;
}
用途是用来处理前端传递过来的日期数据,它和JsonFormat的区别为:前者是处理前端传来的数据,后者是处理后端查询到的日期数据
前端传来的日期格式为:yyyy-MM-dd
package com.dong1024.server.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Component
public class DateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String s) {
return LocalDate.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
}
员工类主要修改的是时间属性的格式和员工表关联的属性,前端需要查询到的数据中包含很多不在Employee实体类中的字段
我们加了表中不存在的这些字段,可以想象处理Employee员工的数据还是比较麻烦的
@ApiModelProperty(value = "民族")
@TableField(exist = false)
private Nation nation;
@ApiModelProperty(value = "政治面貌")
@TableField(exist = false)
private PoliticsStatus politicsStatus;
@ApiModelProperty(value = "部门")
@TableField(exist = false)
private Department department;
@ApiModelProperty(value = "职位等级")
@TableField(exist = false)
private Joblevel joblevel;
@ApiModelProperty(value = "职位")
@TableField(exist = false)
private Position position;
可以看到前端有基本的查询功能,即查询所有员工,也有高级的查询功能,其中,政治面貌,民族,职位,职称,聘用形式,所属部门都在实体类字段中,入职日期是一个范围的查询,需要我们定义一个数组
EmployeeMapper
IPage是mybatisplus里面的分页对象接口,里面又提供了很多的方法,其实现类是mybatisplus里面的Page
/**
* 分页 Page 对象接口
*
* @author hubin
* @since 2018-06-09
*/
public interface IPage<T> extends Serializable {
/**
* 降序字段数组
*
* @return order by desc 的字段数组
* @see #orders()
*/
@Deprecated
default String[] descs() {
return null;
}
/**
* 升序字段数组
*
* @return order by asc 的字段数组
* @see #orders()
*/
@Deprecated
default String[] ascs() {
return null;
}
/**
* 获取排序信息,排序的字段和正反序
*
* @return 排序信息
*/
List<OrderItem> orders();
/**
* KEY/VALUE 条件
*
* @return ignore
*/
default Map<Object, Object> condition() {
return null;
}
/**
* 自动优化 COUNT SQL【 默认:true 】
*
* @return true 是 / false 否
*/
default boolean optimizeCountSql() {
return true;
}
/**
* 进行 count 查询 【 默认: true 】
*
* @return true 是 / false 否
*/
default boolean isSearchCount() {
return true;
}
/**
* 计算当前分页偏移量
*/
default long offset() {
return getCurrent() > 0 ? (getCurrent() - 1) * getSize() : 0;
}
/**
* 当前分页总页数
*/
default long getPages() {
if (getSize() == 0) {
return 0L;
}
long pages = getTotal() / getSize();
if (getTotal() % getSize() != 0) {
pages++;
}
return pages;
}
/**
* 内部什么也不干
* 只是为了 json 反序列化时不报错
*/
default IPage<T> setPages(long pages) {
// to do nothing
return this;
}
/**
* 设置是否命中count缓存
*
* @param hit 是否命中
* @since 3.3.1
*/
default void hitCount(boolean hit) {
}
/**
* 是否命中count缓存
*
* @return 是否命中count缓存
* @since 3.3.1
*/
default boolean isHitCount() {
return false;
}
/**
* 分页记录列表
*
* @return 分页对象记录列表
*/
List<T> getRecords();
/**
* 设置分页记录列表
*/
IPage<T> setRecords(List<T> records);
/**
* 当前满足条件总行数
*
* @return 总条数
*/
long getTotal();
/**
* 设置当前满足条件总行数
*/
IPage<T> setTotal(long total);
/**
* 获取每页显示条数
*
* @return 每页显示条数
*/
long getSize();
/**
* 设置每页显示条数
*/
IPage<T> setSize(long size);
/**
* 当前页,默认 1
*
* @return 当前页
*/
long getCurrent();
/**
* 设置当前页
*/
IPage<T> setCurrent(long current);
/**
* IPage 的泛型转换
*
* @param mapper 转换函数
* @param 转换后的泛型
* @return 转换泛型后的 IPage
*/
@SuppressWarnings("unchecked")
default <R> IPage<R> convert(Function<? super T, ? extends R> mapper) {
List<R> collect = this.getRecords().stream().map(mapper).collect(toList());
return ((IPage<R>) this).setRecords(collect);
}
}
employeeService中的逻辑编写
参数中,我们要传入Employee实体类通过里面的字段来进行条件查询,LocalDate[] beginDateScope是我们查询入职日期的范围,需要用LocalDate数组进行定义,数组中的元素有且必须只能有两个
@Override
public RespPageBean getEmployeeByPage(Integer currentPage, Integer size, Employee employee, LocalDate[] beginDateScope) {
//开启分页
Page<Employee> page = new Page<>(currentPage,size);
IPage<Employee> employeeByPage = employeeMapper.getEmployeeByPage(page, employee, beginDateScope);
RespPageBean respPageBean = new RespPageBean(employeeByPage.getTotal(),employeeByPage.getRecords());
return respPageBean;
}
mapper文件编写:
注:page是不需要加Param注解的,因为我们在数据库查询之前就已经分页成功了,所以不需要加注解
IPage<Employee> getEmployeeByPage(Page<Employee> page, @Param("employee") Employee employee, @Param("beginDateScope") LocalDate[] beginDateScope);
------------------------------------------------------
<resultMap id="EmployeeInfo" type="com.dong1024.server.pojo.Employee" extends="BaseResultMap">
<association property="nation" javaType="com.dong1024.server.pojo.Nation">
<id column="nid" property="id" />
<result column="nname" property="name" />
</association>
<association property="politicsStatus" javaType="com.dong1024.server.pojo.PoliticsStatus">
<id column="pid" property="id" />
<result column="pname" property="name" />
</association>
<association property="department" javaType="com.dong1024.server.pojo.Department">
<id column="did" property="id" />
<result column="dname" property="name" />
</association>
<association property="joblevel" javaType="com.dong1024.server.pojo.Joblevel">
<id column="jid" property="id" />
<result column="jname" property="name" />
</association>
<association property="position" javaType="com.dong1024.server.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>
员工添加时政治面貌,民族,职位,职称都是下拉框,因此需要查询这些参数的所有信息,以便前端调用接口生成下拉列表。工号默认递增,因此查出最大工号,将最大工号+1就是要添加的员工的工号。
注意:我们拿到的是一个List
@ApiOperation("获取工号")
@GetMapping("/maxWorkID")
public RespBean maxWorkID(){
return employeeService.maxWorkID();
}
--------------------------------------------------------
/**
* 获取最大工号
* @return
*/
@Override
public RespBean maxWorkID() {
//通过sql语句查询到最后一个workId
List<Map<String, Object>> maps = employeeMapper.selectMaps(new QueryWrapper<Employee>().select("max(workID)"));
//将查询到的最后一个workId + 1
int maxWorkId = Integer.parseInt(maps.get(0).get("max(workID)").toString()) + 1;
//此格式化目的是不让前面的零消失,比如00000101,如果不设置此格式化,输出结果就为101
String formatMaxWorkId = String.format("%08d", maxWorkId);
return RespBean.success(null,formatMaxWorkId);
}
员工字段中的合同期限是根据合同起始日期和合同终止日期计算出来的,合同期限单位为年,保留两位小数
public RespBean addEmp(Employee employee){
//处理合同期限,单位:年保留两位小数
LocalDate beginContract = employee.getBeginContract();
LocalDate endContract = employee.getEndContract();
//计算合同起始日期和到同终止日期的天数
long days = beginContract.until(endContract, ChronoUnit.DAYS);
DecimalFormat decimalFormat = new DecimalFormat("##.00");
employee.setContractTerm(Double.parseDouble(decimalFormat.format(days/365.00)));
if (1== employeeMapper.insert(employee)){
return RespBean.success("员工数据插入成功!");
}
return RespBean.error("员工数据插入失败!");
}
<!--easy poi依赖-->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.2.0</version>
</dependency>
在Employee员工类上添加 Excel 导出基本注释 @Excel,如果字段长度过长可以添加width属性,如果是时间格式需要加format属性
需要注意的是,我们不需要导出员工表中的民族id,政治面貌id,部门id,职位等级id,职位id,这些字段上面不需要加Excel注解。我们需要导出其民族名字,政治面貌名字,部门名字,职位等级名字,职位名字,这些字段我们都在实体类中设置了关联,需要在这些字段上加@ExcelEntity,标记是不是导出excel 标记为实体类
@ApiOperation(value = "导出员工数据")
@GetMapping(value = "/export", produces = "application/octet-stream")
public void exportEmployee(HttpServletResponse response) {
List<Employee> employeeList = employeeService.getEmployee(null);
ExportParams exportParams = new ExportParams("员工表", "员工表", ExcelType.HSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, Employee.class, employeeList);
ServletOutputStream outputStream = null;
try {
//流形式
response.setHeader("content-type", "application/octet-stream");
//防止中文乱码
response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode("员工表.xls", "UTF-8"));
outputStream = response.getOutputStream();
workbook.write(outputStream); }
catch (Exception e) {
e.printStackTrace();
}finally {
if (outputStream != null){
try {
outputStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
mapper层只需要负责根据员工id查询员工的信息即可,不传id的话就是默认查询所有
@Override
public List<Employee> getEmployee(Integer id) {
return employeeMapper.getEmployee(id);
}
导入时,excel表中没有基于民族,政治面貌,职称,职位的这些字段id,只有这些字段的name属性,那么如何将这些字段的id导入t_employee表中呢—我们要获取到对应的民族id,政治面貌id,职称id,职位id,部门id。
有两种方法
根据每个关联字段的name属性的值去数据库查询对应的id,显然在循环里面不断去查询数据库非常消耗性能,每插入一条数据不仅仅要执行插入语句,还要多执行5次查询,非常耗费性能
重写equals和hashCode方法,只要name属性的值一致就表示对象一致。前提是name属性的值基本不会变动
我们选择第二种方法实现
@ApiOperation("导入员工数据")
@PostMapping("/import")
@Transactional
public RespBean importEmployee(MultipartFile multipartFile) {
ImportParams importParams = new ImportParams();
//设置第一行为标题行,避免系统将标题行作为数据导入
importParams.setTitleRows(1);
//查询民族对象
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> employeeList = ExcelImportUtil.importExcel(multipartFile.getInputStream(), Employee.class, importParams);
//设置关联属性的id值
employeeList.forEach(employee -> {
/**
*
* employeeList.forEach(employee -> {
* //获取每条employee对象中的nation名字在nationList中的索引
* int nationIndex = nationList.indexOf(new Nation(employee.getNation().getName()));
* //获取这个nation名字在nationList中对应索引的id就是我们想设置的id
* Integer nationId = nationList.get(nationIndex).getId();
* employee.setNationId(nationId);
* });
*
*
* employeeList.forEach(employee -> {
* //获取每条employee对象中的nation名字在nationList中的索引,用Nation里定义的有参方法取出id
* //indexOf(String s)返回指定字符在字符串中第一次出现处的索引
* int nationIndex = nationList.indexOf(new Nation(employee.getNation().getName()));
* //通过索引可以获取nation这个对象,然后getId就可以获取id,获取这个nation名字在nationList中对应索引的id就是我们想设置的id
* Integer nationId = nationList.get(nationIndex).getId();
* employee.setNationId(nationId);
* });
*/
//设置nationId
employee.setNationId(nationList.get(nationList.indexOf(new Nation(employee.getNation().getName()))).getId());
//设置politicId
employee.setPoliticId(politicsStatusList.get(politicsStatusList.indexOf(new PoliticsStatus(employee.getPoliticsStatus().getName()))).getId());
//设置departmentId
employee.setDepartmentId(departmentList.get(departmentList.indexOf(new Department(employee.getDepartment().getName()))).getId());
//设置jobLevelId
employee.setJobLevelId(joblevelList.get(joblevelList.indexOf(new Joblevel(employee.getJoblevel().getName()))).getId());
//设置positionId
employee.setPosId(positionList.get(positionList.indexOf(new Position(employee.getPosition().getName()))).getId());
});
if (employeeService.saveBatch(employeeList)){
return RespBean.success("员工导入成功!");
}
return RespBean.error("员工导入失败!");
} catch (Exception e) {
e.printStackTrace();
}
return RespBean.error("员工导入失败!");
}
}
创建一个简单的邮件服务,通过添加员工,添加成功后发送欢迎邮件到该员工的邮箱上
首先创建一个新的服务模块,yeb-mail
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dong1024</groupId>
<artifactId>yeb-mail</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>com.dong1024</groupId>
<artifactId>yeb</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<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.dong1024</groupId>
<artifactId>yeb-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
修改yml配置文件
server:
# 端口
port: 8082
spring:
# 邮件配置
mail:
# 邮件服务器地址
host: smtp.qq.com
# 协议
protocol: smtp
# 编码格式
default-encoding: utf-8
# 授权码(在邮箱开通服务时获取)
password: ypyup
# 发送者邮箱地址
username: 1010251@qq.com
# 端口(不同邮箱端口号不同)
port: 465
#使用了465 SSL 端口,但是不设置ssl,则导致该错误
properties.mail.smtp.ssl.enable: true
# rabbitmq配置
rabbitmq:
# 用户名
username: guest
# 密码
password: guest
# 服务器地址
host: 127.0.0.1
# 端口
port: 5672
listener:
simple:
#开启手动确认
acknowledge-mode: manual
redis:
#超时时间
timeout: 10000ms
#服务器地址
host: 127.0.0.1
#服务器端口
port: 6379
#数据库
database: 0
#密码
password:
lettuce:
pool:
#最大连接数,默认8
max-active: 1024
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接
max-idle: 200
#最小空闲连接
min-idle: 5
修改启动类
主启动类要排除DataSourceAutoConfiguration类,因为该模块不需要连接数据库,但是引入了yeb-server,里面有mysql依赖,需要排除:
package com.dong1024.mail;
import com.dong1024.server.pojo.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 dong
* @since 1.0.0
*/
//排除掉server里的yml文件的sql影响
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class})
public class MailApplication {
public static void main(String[] args) {
SpringApplication.run(MailApplication.class,args);
}
@Bean
public Queue queue(){
return new Queue(MailConstants.MAIL_QUEUE_NAME);
}
}
修改添加员工的方法
在员工添加成功时,我们需要将员工信息添加到rabbitmq的消息队列中,然后在邮件模块,通过rabbitmq将员工信息进行获取消费,获取到员工信息,并将邮件发送给员工邮箱中
准备邮件模板
在resource目录中创建templates目录,模板引擎默认从该目录中找html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.theymeleaf.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>
创建消费者消费rabbitmq中的信息,并发送邮件
package com.zlq.mail.receiver;
import com.zlq.server.pojo.Employee;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Date;
/**
* @ProjectName:yeb
* @Package:com.zlq.mail
* @ClassName: MailReceiver
* @description:
* @author: LiQun
* @CreateDate:2021/5/5 3:19 下午
*/
@Component
public class MailReceiver {
private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class);
@Resource
private JavaMailSender javaMailSender;
@Resource
private MailProperties mailProperties;
@Resource
private TemplateEngine templateEngine;
@RabbitListener(queues = "mail.welcome") //监听mail.welcome队列中的消息
public void sendMail(Employee employee){
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper messageHelper = new MimeMessageHelper(message);
try {
//设置发送人
messageHelper.setFrom(mailProperties.getUsername());
//设置收件人
messageHelper.setTo(employee.getEmail());
//设置发送主体
messageHelper.setSubject("入职欢迎邮件");
//设置发送日期
messageHelper.setSentDate(new Date());
//设置邮件内容
Context context = new Context();
//如下参数对应mail.html中模板引擎的参数
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);
messageHelper.setText(mail,true); //参数1:邮件参数 参数2:是否是html邮件
//发送邮件
javaMailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
LOGGER.error("邮件发送失败 ========>",e.getMessage());
}
}
}
在实际开发中要满足消息投递的可靠性,在rabbitmq中,如何确保消息被正常投递,且被消费?
如何保证消息不会被发丢了?如何保证消息不会被发重复了?
方式一:消息落库,对消息状态进行打标
项目实现流程:
发送消息时,将当前消息数据存入数据库,投递状态为消息投递中 status= 0
开启消息确认回调机制。确认成功,更新投递状态为消息投递成功 status = 1
开启定时任务,重新投递失败的消息。重试超过3次,更新投递状态为投递失败 status = 2
上图的详细步骤解析如下
正常链路流程
step1:生产者生产消息,将消息打标(打标:消息中含有额外的标记字段用来展示消息状态)写入mysql数据库,此时消息的status为0
step2:生产者通过交换机向队列中发送消息
step3:通过消息回调机制监听消息是否成功发送
step4:如果首次发送成功—将消息状态status设为1
异常链路流程
step5:启动一个分布式定时任务,如果首次发送失败,定时任务会定时查询status为0的消息
step6:定时任务查询到status为0的消息就会重新申请发送该消息
step7:定时任务每尝试发送该消息一次,就会将该消息中的count(消息发送次数)加一,当count>=3时,就将status设为2,为消息发送失败
消息打标落库的缺点:对数据库进行二次操作,即插入了业务数据,又插入了消息的记录数据,存在二次db操作,在高并发操作下存在数据库性能瓶颈
方式二:消息延迟投递,做二次确认,回调检查
这里存在上游服务生产端生产消息,和下游服务消费端消费者
将业务数据入库
生产端发送消息到rabbbitmq队列中
延时投递二次发送消息,中间有间隔时间差
消费端收到消息后做确认
消费端发送确认信息到消息队列中,确认该信息是否是重新生成的消息,该消息附带消费端收到的消息的id值
通过回调服务,监听消费端发来的确认消息
如果监听到确认信息,就把消息放到数据库中去
如果监听到二次延时投递的消息,就去数据库中检查是否存在延时投递的这个消息id
如果数据库中存在二次延时投递消息的id,说明消息已投递成功
如果数据库中不存在二次延时投递消息的id,说明消息未投递成功,那就发送RPC通信,重新从第一步开始
方案二主要目的是为了减少数据库操作,提高并发量,在高并发场景下,最关心的不是消息100%投递成功,而是一定要保证性能,保证能抗得住这么大的并发量,所以能减少数据库的操作就尽量减少,可以异步的进行补偿
我们选择简单的方式1
1.定义消息状态常量
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";
}
2.将要发送的消息携带具体参数入mysql数据库
这是我们自己创建的存放消息参数的数据库表
@Override
public RespBean addEmp(Employee employee) {
//处理合同期限,保留两位小数
LocalDate beginContract = employee.getBeginContract();
LocalDate endContract = employee.getEndContract();
//查询的天数
long days = beginContract.until(endContract, ChronoUnit.DAYS);
//处理合同期限,保留两位小数
DecimalFormat decimalFormat = new DecimalFormat("##.00");
employee.setContractTerm(Double.parseDouble(decimalFormat.format(days/365.00)));
if (1==employeeMapper.insert(employee)){
Employee emp = employeeMapper.getEmployee(employee.getId()).get(0);
//数据库记录发送的信息
String msgId = UUID.randomUUID().toString();
MailLog mailLog = new MailLog();
mailLog.setMsgId(msgId);
mailLog.setEid(employee.getId());
mailLog.setStatus(0);
mailLog.setCount(0);
mailLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);
mailLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
mailLog.setTryTime(LocalDateTime.now().plusMinutes(MailConstants.MSG_TIMEOUT));
mailLog.setCreateTime(LocalDateTime.now());
mailLog.setUpdateTime(LocalDateTime.now());
mailLogMapper.insert(mailLog);
//发送信息
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,MailConstants.MAIL_ROUTING_KEY_NAME,emp,
new CorrelationData(msgId));
return RespBean.success("添加成功");
}
return RespBean.error("添加失败");
}
3.修改邮件模块
设置创建的队列名称为常量类中设置的队列名
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class})
public class YebMailApplication {
public static void main(String[] args) {
SpringApplication.run(YebMailApplication.class,args);
}
@Bean
public Queue queue(){
return new Queue(MailConstants.MAIL_QUEUE_NAME);
}
}
4.编写rabbitmq配置类
package com.dong1024.server.config;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.dong1024.server.pojo.MailConstants;
import com.dong1024.server.pojo.MailLog;
import com.dong1024.server.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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* RabbitMQ配置类
*/
@Configuration
public class RabbitMQConfig {
//连接工厂
@Resource
private CachingConnectionFactory cachingConnectionFactory;
@Resource
private IMailLogService mailLogService;
private static final Logger LOGGER = LoggerFactory.getLogger(RabbitMQConfig.class);
//向spring容器中注入RabbitTemplate对象,并配置开启确认消息回调和消息失败回调
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
/**
* 消息确认回调,确认消息是否到达broker
* correlationData:消息唯一标识
* ack:确认结果,消息是否发送成功
* cause:失败原因
*/
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
String msgId = correlationData.getId();
if (ack) {
LOGGER.info("消息发送成功============>" + msgId);
//监听消息是否发送成功,如果成功将状态改为1
mailLogService.update(new UpdateWrapper<MailLog>().set("status", 1).eq("msgId", msgId));
} else {
LOGGER.info("消息发送到queue失败============>" + msgId);
}
});
/**
* 消息失败回调
*/
rabbitTemplate.setReturnsCallback(returnedMessage ->
LOGGER.info("{}=====>消息发送到queue时失败", returnedMessage.getMessage().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);
}
}
5.在配置文件中开启确认消息回调和消息失败回调
6.开启邮件发送的定时任务
为了保证消息的可靠性,我们需要开启邮件发送的定时任务,目的:
注意不要忘了在主启动类上添加定时任务注解@EnableScheduling
业务逻辑为:
package com.dong1024.server.task;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.dong1024.server.pojo.Employee;
import com.dong1024.server.pojo.MailConstants;
import com.dong1024.server.pojo.MailLog;
import com.dong1024.server.service.IEmployeeService;
import com.dong1024.server.service.IMailLogService;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.parameters.P;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
@Component
public class MailTask {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private IMailLogService mailLogService;
@Resource
private IEmployeeService employeeService;
//每十秒发送一次
@Scheduled(cron = "0/10 * * * * ?")
public void mailTask() {
//查询消息状态为0且重试时间小于当前时间的消息,只有满足这个条件的消息才需要重新发送
List<MailLog> mailLogList = mailLogService.list(new QueryWrapper<MailLog>()
.eq("status", 0)
.lt("tryTime", LocalDateTime.now()));
//重试次数超过3次,更新为投递失败,不再重试(status更新为2)
mailLogList.forEach(mailLog -> {
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信息
Employee employee = employeeService.getEmployee(mailLog.getEid()).get(0);
//发送消息
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,
MailConstants.MAIL_ROUTING_KEY_NAME, employee,
new CorrelationData(mailLog.getMsgId()));
});
}
}
测试:
这里修改了插入员工时发送到消息队列中的交换机名字,rabbitmq就不知道要把消息发送到哪里,因为没有对应的交换机!因此,消息就无法成功发送,rabbitmq也就无法监听到消息发送成功的确认消息,那么数据库中消息的status就不会变为1,就要等着定时任务来对消息进行处理,通过定时任务将消息发送到rabbitmq消息队列中
定时队列里的名字是正确的,所以能发送成功
3.首次发送失败以及定时任务也发送失败的情况
三次都未发送成功,消息标记为发送失败
这里将插入员工时发送到消息队列中的交换机名字、以及定时任务中交换机的名字都进行了修改,因此消息首次无法发送成功,定时任务也无法发送成功!定时任务尝试三次,会将status改为2,消息再发送。
在生产中如何保证消息不会被重复消费?如果重复的消息同时进来的话,我们就要进行消息幂等性的校验,使其只消费一条消息
对于使用mysql在高并发下可能会存在性能瓶颈,这里使用redis缓存.首先去 Redis 查看当前消息id是否存在,如果存在说明已经消费,直接返回。如果不存在,正常发送消息,并将消息id存入 Reids 。需要手动确认消息
实现步骤
基本修改思路:
package com.dong1024.mail;
import com.dong1024.server.pojo.MailConstants;
import com.rabbitmq.client.Channel;
import com.dong1024.server.pojo.Employee;
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.internet.MimeMessage;
import java.io.IOException;
import java.util.Date;
/**
* 消息接收者
*
* @author zhoubin
* @since 1.0.0
*/
@Component
public class MailReceiver {
//打印日志信息
private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class);
@Autowired
private JavaMailSender javaMailSender;
@Autowired
private MailProperties mailProperties;
@Autowired
private TemplateEngine templateEngine;
@Autowired
private RedisTemplate redisTemplate;
@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);
//获取消息的msgId
String msgId = (String) headers.get("spring_returned_message_correlation");
HashOperations hashOperations = redisTemplate.opsForHash();
try {
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper messageHelper = new MimeMessageHelper(msg);
//判断redis中是否存在msgId,如果有,直接返回
if (hashOperations.entries("mail_log").containsKey(msgId)) {
LOGGER.error("消息已经被消费========>{}", msgId);
/**
* 手动确认消息
* tag:消息序号
* multiple:是否确认多条
*/
channel.basicAck(tag, false);
return;
}
//设置发送人
messageHelper.setFrom(mailProperties.getUsername());
//设置收件人
messageHelper.setTo(employee.getEmail());
//设置发送主体
messageHelper.setSubject("入职欢迎邮件");
//设置发送日期
messageHelper.setSentDate(new Date());
//设置邮件内容
Context context = new Context();
//如下参数对应mail.html中模板引擎的参数
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);
messageHelper.setText(mail, true); //参数1:邮件参数 参数2:是否是html邮件
//发送邮件
javaMailSender.send(msg);
LOGGER.info("邮件发送成功 ========>");
//邮件发送成功后,将msgId标识存入redis
hashOperations.put("mail_log", msgId, "ok");
//手动确认消息
channel.basicAck(tag, false);
} catch (Exception e) {
/**
* 手动确认消息,拒绝接收到的消息,退回到队列,也就是说如果消息的消费出现异常,会将消息退回到队列中
* @tag 消息序号
* @multiple 是否处理多条
* @requeue 是否要退回到队列,如果是false,消息不会重发,
会把消息打入死信队列。如果是true,会无限次重发导致死循环,不建议加try-catch
*/
try {
channel.basicNack(tag, false, true);
} catch (IOException ioException) {
ioException.printStackTrace();
}
e.printStackTrace();
LOGGER.error("邮件发送失败 ========>", e.getMessage());
}
}
}
查询所有员工工资账套用分页插件,更新用mybatisplus操作即可,设置工资id和指定员工id
@ApiOperation("查询所有员工账套")
@GetMapping("/")
public RespPageBean getEmployeeWithSalary(@RequestParam(defaultValue = "1") Integer currentPage,
@RequestParam(defaultValue = "10") Integer pageSize){
return employeeService.getEmployeeWithSalary(currentPage,pageSize);
}
@ApiOperation("更新员工账套")
@PutMapping("/")
public RespBean updateEmployeeSalary(Integer eid, Integer sid){
if(employeeService.update(new UpdateWrapper<Employee>().set("salaryId",sid).eq("id",eid
))){
return RespBean.success("更新成功");
}
return RespBean.error("更新失败");
}
service查询操作逻辑代码
/**
* 查询所有员工账套
* @param currentPage
* @param pageSize
* @return
*/
@Override
public RespPageBean getEmployeeWithSalary(Integer currentPage, Integer pageSize) {
//开启分页
Page<Employee> page = new Page<>(currentPage,pageSize);
IPage<Employee> employeeIPage = employeeMapper.getEmployeeWithSalary(page);
//获取总页数和employee对象
RespPageBean respPageBean = new RespPageBean(employeeIPage.getTotal(),employeeIPage.getRecords());
return respPageBean;
}
mapper:
<resultMap id="EmployeeWithSalary" type="com.dong1024.server.pojo.Employee" extends="BaseResultMap">
<association property="salary" javaType="com.dong1024.server.pojo.Salary">
<id column="sid" property="id" />
<result column="sname" property="name" />
<result column="sbasicSalary" property="basicSalary" />
<result column="sbonus" property="bonus" />
<result column="slunchSalary" property="lunchSalary" />
<result column="strafficSalary" property="trafficSalary" />
<result column="sallSalary" property="allSalary" />
<result column="spensionBase" property="pensionBase" />
<result column="spensionPer" property="pensionPer" />
<result column="smedicalBase" property="medicalBase" />
<result column="smedicalPer" property="medicalPer" />
<result column="saccumulationFundBase" property="accumulationFundBase" />
<result column="saccumulationFundPer" property="accumulationFundPer" />
</association>
<association property="department" javaType="com.dong1024.server.pojo.Department">
<result column="dname" property="name" />
</association>
</resultMap>
<!--获取所有员工账套-->
<select id="getEmployeeWithSalary" resultMap="EmployeeWithSalary">
SELECT
e.*,
d.`name` AS dname,
s.id AS sid,
s.`name` AS sname,
s.basicSalary AS sbasicSalary,
s.bonus AS sbouns,
s.lunchSalary AS slunchSalary,
s.trafficSalary AS strafficSalary,
s.allSalary AS sallSalary,
s.pensionBase AS spensionBase,
s.pensionPer AS spensionPer,
s.medicalBase AS smedicalBase,
s.medicalPer AS smedicalPer,
s.accumulationFundBase AS saccumulationFundBase,
s.accumulationFundPer AS saccumulationFundPer
FROM
t_employee e
LEFT JOIN t_salary s ON e.salaryId = s.id
LEFT JOIN t_department d ON e.departmentId = d.id
ORDER BY
e.id
</select>
1.更新操作用户信息
@RestController
public class AdminInfoController {
@Autowired
private IAdminService adminService;
@ApiOperation("更新当前用户信息")
@PutMapping("/admin/info")
public RespBean updateAdminInfo(@RequestBody Admin admin, Authentication authentication){
if (adminService.updateById(admin)){
//每次更新成功后都要重新设置Authentication对象
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(admin,
null,authentication.getAuthorities()));
return RespBean.success("更新成功");
}
return RespBean.error("更新失败");
}
2.修改用户密码
因为修改密码需要老密码跟新密码,必须把老密码写正确。才能修改密码
@ApiOperation("更新用户密码")
@PutMapping(value = "/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");
return adminService.updateAdminPassword(oldPass,pass,adminId);
}
service逻辑实现:
/**
* 更新用户密码
*
* @param oldPass
* @param pass
* @param adminId
* @return
*/
@Override
public RespBean updateAdminPassword(String oldPass, String pass, Integer adminId) {
Admin admin = adminMapper.selectById(adminId);
//springSecurity加密了密码,所以要解码
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//比较输入的密码跟老密码是否一致,一致的话才能修改密码
if (encoder.matches(oldPass, admin.getPassword())) {
//加密新密码
String newPassword = encoder.encode(pass);
admin.setPassword(newPassword);
if (adminMapper.updateById(admin) == 1) {
return RespBean.success("密码修改成功!");
}
return RespBean.error("密码修改失败!");
}
return RespBean.error("密码输入错误!");
}
测试发现出现异常
原因:这是我们的 Admin 实体类实现了 UserDetails 接口,重写了 getAuthorities() 方法,但是Admin 实体类却没有对应的 Collection extends GrantedAuthority> 属性,也无法创建含有Collection extends GrantedAuthority> 属性的构造函数。JSON无法进行反序列化,导致报错
解决方案:自定义反序列化类,在 getAuthorities() 方法定义使用自定义的反序列化类进行
package com.dong1024.server.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;
/**
* @ProjectName:yeb
* @ClassName: AdminAuthorityDeserializer
* @description: 自定义Authority的Json反序列化解析器
* @author: dong
*/
public class CustomAuthorityDeserializer extends JsonDeserializer {
@Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
JsonNode jsonNode = mapper.readTree(jsonParser);
List<GrantedAuthority> authorityList = new LinkedList<>();
//迭代器
Iterator<JsonNode> elements = jsonNode.elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
authorityList.add(new SimpleGrantedAuthority(authority.asText()));
}
return authorityList;
}
}
在Admin类中的getAuthorities方法上加入注解
版权声明:本文有一部分参考了CSDN博主「宇泽希」的原创文章
原文链接:https://blog.csdn.net/weixin_49149614/article/details/116940265