定制app开发基于SpringBoot + MyBatis的前后端分离实现在线办公系统

定制app开发在线办公系统

目录

1.定制app开发开发环境的搭建及项目介绍

定制app开发本项目目的是实现中小定制app开发型企业的在线办公系统,云E定制app开发办在线办公系统是一个定制app开发用来管理日常的办公事定制app开发务的一个系统

使用SpringSecurity定制app开发做安全认证及权限管理,Redis做缓存,定制app开发做邮件的发送,使用EasyPOI定制app开发实现对员工数据的导入和导出,使用WebSocket定制app开发做在线聊天

定制app开发使用验证码登录

页面展示:

  1. 添加依赖

  2. 使用的AutoGenerator自动生成mapper,service,Controller

2.定制app开发登录模块及配置框架搭建

<1>Jwt定制app开发工具类及对Token的处理

1.1定制app开发根据用户信息生成Token

  1. 定义JWT定制app开发负载中用户名的Key定制app开发以及创建时间的Key

//用户名的keyprivate static final String CLAIM_KEY_USERNAME="sub";//定制app开发签名的时间private static final String CLAIM_KEY_CREATED="created";
  • 1
  • 2
  • 3
  • 4
  1. 定制app开发从配置文件中拿到Jwt定制app开发的密钥和失效时间

/** * @Value定制app开发的值有两类: * ① ${ property : default_value } * ② #{ obj.property? :default_value } * 定制app开发第一个注入的是外部配定制app开发置文件对应的property,定制app开发第二个则是SpEL定制app开发表达式对应的内容。 那个 * default_value,定制app开发就是前面的值为空时的默认值。定制app开发注意二者的不同,#{}里面那个obj代表对象。 *///JWT密钥@Value("${jwt.secret}")private  String secret;//JWT失效时间@Value("${jwt.expiration}")private Long expiration;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  1. 根据用户信息UserDetials生成Token

/** * 根据用户信息生成Token * @param userDetails * @return */public String generateToken(UserDetails userDetails){    //荷载    Map<String,Object> claim=new HashMap<>();    claim.put(CLAIM_KEY_USERNAME,userDetails.getUsername());    claim.put(CLAIM_KEY_CREATED,new Date());    return generateToken(claim);}/** * 根据负载生成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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

1.2根据Token生成用户名

/** * 根据Token生成用户名 * @param token * @return */public String getUsernameFormToken(String token){    String username;    //根据Token去拿荷载    try {        Claims claim=getClaimFromToken(token);        username=claim.getSubject();//获取用户名    } catch (Exception e) {        e.printStackTrace();        username=null;    }    return username;}/** * 从Token中获取荷载 * @param token * @return */private Claims getClaimFromToken(String token) {    Claims claims=null;    try {        claims=Jwts.parser()                .setSigningKey(secret)                .parseClaimsJws(token)                .getBody();    } catch (Exception e) {        e.printStackTrace();    }    return claims;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

1.3判断Token是否有效

/** * 判断Token是否有效 * Token是否过期 * Token中的username和UserDetails中的username是否一致 * @param token * @param userDetails * @return */public boolean TokenIsValid(String token,UserDetails userDetails){    String username = getUsernameFormToken(token);    return username.equals(userDetails.getUsername()) && !isTokenExpired(token);}/** * 判断Token是否过期 * @param token * @return */private boolean isTokenExpired(String token) {    //获取Token的失效时间    Date expireDate=getExpiredDateFromToken(token);    //在当前时间之前,则失效    return expireDate.before(new Date());}/** * 获取Token的失效时间 * @param token * @return */private Date getExpiredDateFromToken(String token) {    Claims claims = getClaimFromToken(token);    return claims.getExpiration();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

1.4判断Token是否可以被刷新

/** * 判断token是否可用被刷新 * 如果已经过期了,则可用被刷新,未过期,则不可用被刷新 * @param token * @return */public boolean canRefresh(String token){    return !isTokenExpired(token);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

1.5刷新Token,获取新的Token

/** * 刷新Token * @param token * @return */public String refreshToken(String token){    Claims claims=getClaimFromToken(token);    claims.put(CLAIM_KEY_CREATED,new Date());    return generateToken(claims);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

<2>登录功能的实现

  • Controller层

    @ApiOperation(value = "登录之后返回token")@PostMapping("/login")//AdminLoginParam 自定义登录时传入的对象,包含账号,密码,验证码 public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){    return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request);}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • Service层

    /** * 登录之后返回token * @param username * @param password * @param request * @return */@Overridepublic RespBean login(String username, String password,String code, HttpServletRequest request) {    String captcha = (String)request.getSession().getAttribute("captcha");//验证码功能,后面提到    //验证码为空或匹配不上    if((code == null || code.length()==0) || !captcha.equalsIgnoreCase(code)){        return RespBean.error("验证码错误,请重新输入");    }    //通过username在数据库查出这个对象    //在SecurityConfig配置文件中,重写了loadUserByUsername方法,返回了userDetailsService Bean对象,使用我们自己的登录逻辑    UserDetails userDetails = userDetailsService.loadUserByUsername(username);    //如果userDetails为空或userDetails中的密码和传入的密码不相同    if (userDetails == null||!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<>();    tokenMap.put("token",token);    tokenMap.put("tokenHead",tokenHead);//tokenHead,从配置文件yml中拿到的token的请求头 == Authorization    return RespBean.success("登陆成功",tokenMap);//将Token返回}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

<3>退出登录

退出登录功能由前端实现,我们只需要返回一个成功信息即可

@ApiOperation(value = "退出登录")@PostMapping("/logout")/** * 退出登录 */public RespBean logout(){    return RespBean.success("注销成功");}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

<4>获取当前登录用户信息

  • Controller层

     @ApiOperation(value = "获取当前登录用户的信息")    @GetMapping("/admin/info")    public Admin getAdminInfo(Principal principal){        //可通过principal对象获取当前登录对象        if(principal == null){            return null;        }        //当前用户的用户名        String username = principal.getName();        Admin admin= adminService.getAdminByUsername(username);        //不能返回前端用户密码,设置为空        admin.setPassword(null);        //将用户角色返回        admin.setRoles(adminService.getRoles(admin.getId()));        return admin;    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

<5>SpringSecurity的配置类SecurityConfig

5.1 覆盖SpringSecurity默认生成的账号密码,并让他走我们自定义的登录逻辑

//让SpringSecurity走我们自己登陆的UserDetailsService逻辑//认证信息的管理 用户的存储 这里配置的用户信息会覆盖掉SpringSecurity默认生成的账号密码@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {    auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());}//密码加解密@Beanpublic PasswordEncoder passwordEncoder(){    return new BCryptPasswordEncoder();}@Override@Bean  //注入到IOC中,在登录时使用到的userDetailsService就是这个Bean,loadUserByUsername方法是这里重写过的public UserDetailsService userDetailsService(){    return username->{        Admin admin=adminService.getAdminByUsername(username);        if(admin != null){            admin.setRoles(adminService.getRoles(admin.getId()));            return admin;        }        throw new UsernameNotFoundException("用户名或密码错误");    };}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

登录功能中使用的userDetailsService对象由这里注入,重写loadUserByUsername方法实现自定义登录逻辑

5.2进行资源的拦截,权限设置,登录过滤器设置

@Overrideprotected void configure(HttpSecurity http) throws Exception {    //使用Jwt不需要csrf    http.csrf().disable()            //基于token,不需要Session            .sessionManagement()            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)            .and()            //授权认证            .authorizeRequests()            .antMatchers("/doc.html").permitAll()            //除了上面,所有的请求都要认证            .anyRequest()            .authenticated()            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {                //动态权限配置                @Override                public <O extends FilterSecurityInterceptor> O postProcess(O o) {                    o.setAccessDecisionManager(customUrlDecisionManager);                    o.setSecurityMetadataSource(customFilter);                    return o;                }            })            .and()            //禁用缓存            .headers()            .cacheControl();    //添加jwt登录授权过滤器  判断是否登录    http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);    //添加自定义未授权和未登录结果返回    http.exceptionHandling()        //权限不足            .accessDeniedHandler(restfulAccessDeniedHandler)        //未登录            .authenticationEntryPoint(restAuthorizationEntryPoint);}//将登录过滤器注入@Beanpublic JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){    return new JwtAuthencationTokenFilter();}//需要放行的资源@Overridepublic void configure(WebSecurity web) throws Exception {    web.ignoring().antMatchers(            "/login",            "/logout",            "/css/**",            "/js/**",            //首页            "/index.html",            //网页图标            "favicon.ico",            //Swagger2            "/doc.html",            "/webjars/**",            "/swagger-resources/**",            "/v2/api-docs/**",            //放行图像验证码            "/captcha",            //WebSocket            "/ws/**"    );}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
5.2.1登录过滤器的配置
public class JwtAuthencationTokenFilter extends OncePerRequestFilter {   //Jwt存储头    @Value("${jwt.tokenHeader}")    private String tokenHeader;    //Jwt头部信息    @Value("${jwt.tokenHead}")    private String tokenHead;    @Autowired    private JwtTokenUtil jwtTokenUtil;    @Autowired    private UserDetailsService userDetailsService;    @Override    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {        //token存储在Jwt的请求头中        //通过key:tokenHeader拿到value:token        //这里我们定义的token后期以:Bearer开头,空格分割,加上真正的jwt        //通过tokenHeader(Authorization)拿到以Bearer开头 空格分割 加上真正的jwt的字符串        String authHeader = httpServletRequest.getHeader(tokenHeader);        //判断这个token的请求头是否为空且是以配置信息中要求的tokenHead开头        if(authHeader != null && authHeader.startsWith(tokenHead)){            //截取真正的jwt            String authToken=authHeader.substring(tokenHead.length());            String username=jwtTokenUtil.getUsernameFormToken(authToken);            //token存在用户名但是未登录            if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){                //登录                UserDetails userDetails = userDetailsService.loadUserByUsername(username);                //验证token是否有效,重新设置用户对象                if(jwtTokenUtil.TokenIsValid(authToken,userDetails)){                    //把对象放到Security的全局中                    UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());                    //将请求中的Session等信息放入Details,再放入Security全局中                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));                SecurityContextHolder.getContext().setAuthentication(authenticationToken);                }            }        }        //放行        filterChain.doFilter(httpServletRequest,httpServletResponse);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
5.2.2添加未登录结果处理器

当未登录或者Token失效时访问未放行的接口时,自定义返回的结果

@Componentpublic class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {    @Override    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {        httpServletResponse.setCharacterEncoding("UTF-8");        httpServletResponse.setContentType("application/json");        PrintWriter out = httpServletResponse.getWriter();        RespBean bean=RespBean.error("尚未登录,请登录");        bean.setCode(401);        out.write(new ObjectMapper().writeValueAsString(bean));        out.flush();        out.close();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
5.2.3添加权限不足结果处理器

当访问接口没有权限时,自定义返回结果

@Componentpublic class RestfulAccessDeniedHandler implements AccessDeniedHandler {    @Override    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {        httpServletResponse.setCharacterEncoding("UTF-8");        httpServletResponse.setContentType("application/json");        PrintWriter out = httpServletResponse.getWriter();        RespBean bean=RespBean.success("权限不足,请联系管理员");        bean.setCode(401);        out.write(new ObjectMapper().writeValueAsString(bean));        out.flush();        out.close();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
5.2.4添加权限控制器,根据请求的URL确定访问该URL需要什么角色
@Componentpublic class CustomFilter implements FilterInvocationSecurityMetadataSource {    @Autowired    private IMenuService menuService;    AntPathMatcher antPathMatcher=new AntPathMatcher();    @Override    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {        //获取请求的URL        String requestUrl = ((FilterInvocation) o).getRequestUrl();        List<Menu> menus = menuService.getMenuWithRole();        //将URL所需要的角色放入Menu中        for (Menu menu:menus) {            //判断请求Url与菜单角色拥有的url是否匹配            if(antPathMatcher.match(menu.getUrl(),requestUrl)){                // 该Url所需要的角色                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<?> aClass) {        return false;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
5.2.5添加权限控制器,对角色信息进行处理,是否可用访问URL
@Componentpublic class CustomUrlDecisionManager implements AccessDecisionManager {    @Autowired    private CustomFilter customFilter;    @Override    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {        for (ConfigAttribute configAttribute: collection) {            // 当前url所需要的角色            List<ConfigAttribute> list= (List<ConfigAttribute>) customFilter.getAttributes(o);            String[] needRoles=new String[list.size()];            for (int i = 0; i <list.size() ; i++) {                needRoles[i]=list.get(i).getAttribute();            }            //判断角色是否登录即可访问的角色,此角色在CustomFilter中设置            for (String needRole:needRoles) {                if ("ROLE_LOGIN".equals((needRole))) {                    //判断是否已经登录                    if(authentication instanceof AnonymousAuthenticationToken){                        throw new AccessDeniedException("尚未登录,请登录");                    }else {                        return;                    }                }            }            //判断用户角色是否为url所需要的角色            //得到用户拥有的角色  这里在Admin类中已经将用户的角色放入了            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();            for (String needRole:needRoles) {                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;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

<6>Swagger2的配置

@Configuration@EnableSwagger2public class Swagger2Config {    @Bean    public Docket createRestApi(){        return new Docket(DocumentationType.SWAGGER_2)                //基础设置                .apiInfo(apiInfo())                //扫描哪个包                .select()                .apis(RequestHandlerSelectors.basePackage("org.example.server.controller"))                //任何路径都可以                .paths(PathSelectors.any())                .build()                .securityContexts(securityContexts())                .securitySchemes(securitySchemes());    }    private ApiInfo apiInfo(){        return new ApiInfoBuilder()                .title("云E办接口文档")                .description("云E办接口文档")                .contact(new Contact("朱云飞", "http:localhost:8081/doc.html","2690534598@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)) //带有pathRegex字段的接口访问不带添加的Authorization全局变量                .build();    }    //添加Swagger全局的Authorization  全局认证    固定的代码    private List<SecurityReference> defaultAuth() {        List<SecurityReference> result=new ArrayList<>();        //设置范围为全局        AuthorizationScope authorizationScope=new AuthorizationScope("global","accessEeverything");        AuthorizationScope[]authorizationScopes=new AuthorizationScope[1];        authorizationScopes[0]=authorizationScope;        result.add((new SecurityReference("Authorization",authorizationScopes)));//这里的Authorization和上文ApiKey第二个参数一致        return  result;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

注意:

 ApiKey apiKey=new ApiKey("Authorization", "Authorization","Header");
  • 1

<7>验证码功能(这里使用谷歌的验证码Captcha)

7.1验证码的配置类

@Componentpublic 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;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

7.2验证码的控制器

@RestControllerpublic 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(outputStream !=null){                try {                    outputStream.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }        //-------------------生成验证码 end --------------------------    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

<8>根据用户ID查询用户所拥有操控权限的菜单列表

  • Controller层

    @ApiOperation(value = "通过用户ID查询菜单列表")@GetMapping("/menu")public List<Menu> getMenuByAdminId(){    return menuService.getMenuByAdminId();}
    • 1
    • 2
    • 3
    • 4
    • 5
  • Service层

    @Overridepublic List<Menu> getMenuByAdminId() {    //从Security全局上下文中获取当前登录用户Admin    Admin admin= AdminUtil.getCurrentAdmin();    Integer adminId=admin.getId();    ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue();    //从Redis获取菜单数据    List<Menu> menus = (List<Menu>) valueOperations.get("menu_" + adminId);    //如果为空,从数据库中获取    if(CollectionUtils.isEmpty(menus)){        menus=menuMapper.getMenuByAdminId(adminId);        //查询之后放入Redis        valueOperations.set("menu_"+adminId,menus);    }    return menus;}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
  • Mapper层

    <!-- 根据用户id查询菜单列表  --><select id="getMenuByAdminId" 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

<9>使用Redis缓存根据用户ID查出来的菜单信息

9.1 Redis的配置类

@Configurationpublic 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;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

<10>全局异常的统一处理

@RestControllerAdvicepublic class GlobalException {    @ExceptionHandler(SQLException.class)    public RespBean respBeanMysqlException(SQLException e){        if(e instanceof SQLIntegrityConstraintViolationException){            return RespBean.error("该数据有关联数据,操作失败");        }        e.printStackTrace();        return RespBean.error("数据库异常,操作失败");    }    @ExceptionHandler(DateException.class)    public RespBean respBeanDateException(DateException e){        e.printStackTrace();        return RespBean.error(e.getMessage());    }    @ExceptionHandler(Exception.class)    public RespBean respBeanException(Exception e){        e.printStackTrace();        return RespBean.error("未知错误,请联系管理员");    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

3.基础信息设置模块

职位,职称,权限组管理仅涉及单表的增删查改,这里不多写

<1>部门管理

1.1获取所有部门

Mapper层:涉及父子类,递归查找

<select id="getAllDepartments" resultMap="DepartmentWithChildren">    select    <include refid="Base_Column_List"/>    from t_department    where parentId=#{parentId}</select><!-- 通用查询映射结果 -->    <resultMap id="BaseResultMap" type="org.example.server.pojo.Department">        <id column="id" property="id" />        <result column="name" property="name" />        <result column="parentId" property="parentId" />        <result column="depPath" property="depPath" />        <result column="enabled" property="enabled" />        <result column="isParent" property="isParent" />    </resultMap>    <resultMap id="DepartmentWithChildren" type="org.example.server.pojo.Department" extends="BaseResultMap">        <collection property="children" ofType="org.example.server.pojo.Department" select="org.example.server.mapper.DepartmentMapper.getAllDepartments"        column="id">        </collection>    </resultMap>    <!-- 通用查询结果列 -->    <sql id="Base_Column_List">        id, name, parentId, depPath, enabled, isParent    </sql>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

1.2 添加部门

<!--添加部门 --><!--statementType="CALLABLE 调用存储过程--><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>
  • 1
  • 2
  • 3
  • 4
  • 5

1.3删除部门

<!--添加部门 --><!--statementType="CALLABLE 调用存储过程--><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>
  • 1
  • 2
  • 3
  • 4
  • 5

4.薪资模块及薪资管理模块

这里仅介绍获取全部操作员及操作员角色的更新,其他功能都是单表简单的增删查改

<1>获取全部操作员

  • Controller层

    @ApiOperation(value = "获取所有操作员")@GetMapping("/")public List<Admin> getAllAdmins(String keywords){    return adminService.getAllAdmins(keywords);}
    • 1
    • 2
    • 3
    • 4
    • 5
  • Service层

    /** * 获取所有操作员 * @param keywords */@Overridepublic List<Admin> getAllAdmins(String keywords) {    //要传当前登录的Id,当前操作员不用查    return adminMapper.getAllAdmins(AdminUtil.getCurrentAdmin().getId(),keywords);}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • Mapper层

    <!--获取所有操作员 --><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>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    涉及操作员角色的查询

<2>操作员角色的修改

Service层:

/** * 更新操作员角色 * @param adminId * @param rids * @return */@Override@Transactionalpublic RespBean updateAdminRole(Integer adminId, Integer[] rids) {    //先将已经拥有的角色全部删除    adminRoleMapper.delete(new QueryWrapper<AdminRole>().eq("adminId",adminId));    //再将传过来的所有角色添加    Integer result = adminRoleMapper.addAdminRole(adminId, rids);    if(result == rids.length){        return RespBean.success("修改角色成功");    }    return RespBean.error("更新角色失败");}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

思想:先将操作员所有的角色都删除,再将前端闯入的角色全部添加

5.员工模块管理

<1>分页获取全部员工信息

  • Controller

    @ApiOperation(value = "查询所有的员工(分页)")@GetMapping("/")//beginDateScope入职的日期范围public RespPageBean getEmployee(@RequestParam(defaultValue = "1") Integer currentPage,                                @RequestParam(defaultValue = "10") Integer size,                                Employee employee,                                LocalDate[] beginDateScope){    return employeeService.getEmployeeByPage(currentPage,size,employee,beginDateScope);}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • Service层

    @Overridepublic RespPageBean getEmployeeByPage(Integer currentPage, Integer size, Employee employee, LocalDate[] beginDateScope) {    Page<Employee> page=new Page<>(currentPage,size);    IPage<Employee> iPage=employeeMapper.getEmployeeByPage(page,employee,beginDateScope);    RespPageBean respPageBean=new RespPageBean();    respPageBean.setTotal(iPage.getTotal());    respPageBean.setData(iPage.getRecords());    return respPageBean;}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • Mapper层

        <resultMap id="EmployeeInfo" type="org.example.server.pojo.Employee" extends="BaseResultMap">        <association property="nation" javaType="org.example.server.pojo.Nation">            <id column="nid" property="id" />            <result column="nname" property="name" />        </association>        <association property="politicsStatus" javaType="org.example.server.pojo.PoliticsStatus">            <id column="pid" property="id" />            <result column="pname" property="name" />        </association>        <association property="department" javaType="org.example.server.pojo.Department">            <id column="did" property="id" />            <result column="dname" property="name" />        </association>        <association property="joblevel" javaType="org.example.server.pojo.Joblevel">            <id column="jid" property="id" />            <result column="jname" property="name" />        </association>        <association property="position" javaType="org.example.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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

<2>使用EasyPOI对员工信息进行导入和导出

2.1EasyPOI注解的使用

用于员工数据导入:Excel表中的部门,职称等字段在数据库员工表中找不到字段,数据库中是以id外键字段存储

2.2 员工数据的导出

@ApiOperation(value = "导出员工数据")@GetMapping(value = "/export",produces = "application/octet-stream")public void exportEmployee(HttpServletResponse response){    List<Employee> list = employeeService.getEmployee(null);    //参数:文件名,表名,导出的Excel的类型(03版本)    ExportParams params=new ExportParams("员工表","员工表", ExcelType.HSSF);    Workbook workbook = ExcelExportUtil.exportExcel(params, Employee.class, list);    //输入workbook    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(out != null){            try {                out.close();            } catch (IOException e) {                e.printStackTrace();            }        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

2.3 员工数据的导入

@ApiOperation(value = "导入员工数据")@PostMapping("/import")public RespBean importEmployee(MultipartFile file){    //准备导入的数据表    ImportParams params=new ImportParams();    //去掉第一行:标题行    params.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 {        //将Excel表变为List        List<Employee> list = ExcelImportUtil.importExcel(file.getInputStream(), Employee.class, params);        list.forEach(employee -> {            //获取民族ID            Integer nationId = nationList.get(nationList.indexOf(new Nation(employee.getNation().getName()))).getId();            employee.setNationId(nationId);            //获取政治面貌Id            Integer politicsStatusId=politicsStatusList.get(politicsStatusList.indexOf(new PoliticsStatus(employee.getPoliticsStatus().getName()))).getId();            employee.setPoliticId(politicsStatusId);            //获取部门Id            Integer departmentId=departmentList.get(departmentList.indexOf(new Department(employee.getDepartment().getName()))).getId();            employee.setDepartmentId(departmentId);            //获取职称Id            Integer joblevelId=joblevelList.get(joblevelList.indexOf(new Joblevel(employee.getJoblevel().getName()))).getId();            employee.setJobLevelId(joblevelId);            //获取职位Id            Integer positionId=positionList.get(positionList.indexOf(new Position(employee.getPosition().getName()))).getId();            employee.setPosId(positionId);        });        if(employeeService.saveBatch(list)){            return RespBean.success("导入成功");        }    } catch (Exception e) {        e.printStackTrace();    }    return RespBean.error("导入失败");}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

<3>使用RabbitMQ对新入职的员工发送欢迎邮件

这里使用SMTP:需要先去邮箱开通SMTP服务

3.1 RabbitMQ消息发送的可靠性

  1. 消息落库,对消息状态进行标记

步骤:

  • 发送消息时,将当前消息数据存入数据库,投递状态为消息投递中

  • 开启消息确认回调机制。确认成功,更新投递状态为消息投递成功

  • 开启定时任务,重新投递失败的消息。重试超过3次,更新投递状态为投递失败

    1. 消息延迟投递,做二次确认,回调检查

步骤:

  • 发送消息时,将当前消息存入数据库,消息状态为消息投递
  • 过一段时间进行第二次的消息发送
  • 开启消息回调机制,当第一次发送的消息被成功消费时,消费端的确认会被MQ Broker监听,成功则将消息队列中的状态变为投递成功
  • 如果消息投递没有成功,则过一段时间第二次发送的消息也会被MQ Broker监听到,会根据这条消息的ID去消息数据库查找,如果发现消息数据库中的状态为投递中而不是投递成功,则会通知消息放松端重新进行步骤一

3.2消息功能的实现

在进行新员工插入成功后,对新员工发出邮件,并将发送的邮件保存到数据库中

    //获取合同开始和结束的时间    LocalDate beginContact=employee.getBeginContract();    LocalDate endContact=employee.getEndContract();    long days = beginContact.until(endContact, ChronoUnit.DAYS);    //保留两位小数    DecimalFormat decimalFormat=new DecimalFormat("##.00");    employee.setContractTerm(Double.parseDouble(decimalFormat.format(days/365.00)));    if(employeeMapper.insert(employee) == 1){        //获取新插入的员工对象        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);        //消息的状态保存在Model中        mailLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);        mailLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);        mailLog.setCount(MailConstants.MAX_TRY_COUNT);        mailLog.setTryTime(LocalDateTime.now().plusMinutes(MailConstants.MAX_TRY_COUNT));        mailLog.setCreateTime(LocalDateTime.now());        mailLog.setUpdateTime(LocalDateTime.now());        mailLogMapper.insert(mailLog);        //发送信息        //发送交换机,路由键,用户对象和消息ID        rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,                MailConstants.MAIL_ROUTING_KEY_NAME,                emp,                new CorrelationData(msgId));        return RespBean.success("添加成功");    }    return RespBean.error("添加失败");}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

消费端的处理,这里我们使用上述第一种方式,—>消息落库,对消息状态进行标记. 为保证消费者不重复消费同一消息,采取 消息序号+我们传入的消息msgId来识别每一个消息

@Componentpublic 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)    //拿取Message 和 channel 可以拿到 消息序号鉴别消息是否统一个消息多收    通过消息序号+msgId两个来鉴别    public void handler(Message message, Channel channel) {        Employee employee = (Employee) message.getPayload();        MessageHeaders headers = message.getHeaders();        //消息序号        long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);        //拿到存取的UUID        String msgId = (String) headers.get("spring_returned_message_correlation");//这个key固定        HashOperations hashOperations = redisTemplate.opsForHash();        try {            //从Redis中拿取,如果存在,说明消息已经发送成功了,这里直接确认返回            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());            //收件人            helper.setTo(employee.getEmail());            //主题            helper.setSubject("入职欢迎邮件");            //发送日期            helper.setSentDate(new Date());            //邮件内容            Context context = new Context();            //用于theymeleaf获取            context.setVariable("name", employee.getName());            context.setVariable("posName", employee.getPosition().getName());            context.setVariable("joblevelName", employee.getJoblevel().getName());            context.setVariable("departmentName", employee.getDepartment().getName());            //将准备好的theymeleaf模板中的信息转为String            String mail = templateEngine.process("mail", context);            helper.setText(mail, true);            //发送邮件            javaMailSender.send(msg);            LOGGER.info("邮件发送成功");            //将消息id存入redis            //mail_log是Redis  hash的key   msgId是真正的key  "OK"是Value,主要是拿到msgId,"OK"没啥用            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) {                LOGGER.error("邮件发送失败=========>{}", e.getMessage());            }            LOGGER.error("邮件发送失败=========>{}", e.getMessage());        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

消息的配置类,确认应答等

@Configurationpublic 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);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

6.在线聊天功能的实现

这里使用WebSocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连

接,并进行双向数据传输。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正

的双向平等对话,属于服务器推送技术的一种。

WebSocket的配置

这里主要是前端实现,后端只是增加一些配置

@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {    @Value("${jwt.tokenHead}")    private String tokenHead;    @Autowired    private JwtTokenUtil jwtTokenUtil;    @Autowired    private UserDetailsService userDetailsService;    /**     * 添加这个Endpoint,这样在网页可以通过websocket连接上服务     * 也就是我们配置websocket的服务地址,并且可以指定是否使用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配置     * @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())){                    //拿取Token                    String token = accessor.getFirstNativeHeader("Auth-Token");//参数前端已经固定                    if (!StringUtils.isEmpty(token)){                        String authToken = token.substring(tokenHead.length());                        String username = jwtTokenUtil.getUsernameFormToken(authToken);                        //token中存在用户名                        if (!StringUtils.isEmpty(username)){                            //登录                            UserDetails userDetails = userDetailsService.loadUserByUsername(username);                            //验证token是否有效,重新设置用户对象                            if (jwtTokenUtil.TokenIsValid(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");    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发