<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于 SpringBoot + MyBatis 前后端分離實現(xiàn)的在線辦公系統(tǒng)

          共 13635字,需瀏覽 28分鐘

           ·

          2022-01-21 04:45

          點擊關(guān)注公眾號

          1.開發(fā)環(huán)境的搭建及項目介紹

          本項目目的是實現(xiàn)中小型企業(yè)的在線辦公系統(tǒng),云E辦在線辦公系統(tǒng)是一個用來管理日常的辦公事務的一個系統(tǒng)。

          使用SpringSecurity做安全認證及權(quán)限管理,Redis做緩存,RabbitMq做郵件的發(fā)送,使用EasyPOI實現(xiàn)對員工數(shù)據(jù)的導入和導出,使用WebSocket做在線聊天。

          使用驗證碼登錄

          頁面展示:

          • 添加依賴
          • 使用MyBatis的AutoGenerator自動生成mapper,service,Controller

          2.登錄模塊及配置框架搭建

          <1>Jwt工具類及對Token的處理
          • 根據(jù)用戶信息生成Token

          定義JWT負載中用戶名的Key以及創(chuàng)建時間的Key

          //用戶名的key
          private?static?final?String?CLAIM_KEY_USERNAME="sub";
          //簽名的時間
          private?static?final?String?CLAIM_KEY_CREATED="created";

          從配置文件中拿到Jwt的密鑰和失效時間

          /**
          ?*?@Value的值有兩類:
          ?*?①?${?property?:?default_value?}
          ?*?②?#{?obj.property??:default_value?}
          ?*?第一個注入的是外部配置文件對應的property,第二個則是SpEL表達式對應的內(nèi)容。?那個
          ?* default_value,就是前面的值為空時的默認值。注意二者的不同,#{}里面那個obj代表對象。
          ?*/

          //JWT密鑰
          @Value("${jwt.secret}")
          private??String?secret;

          //JWT失效時間
          @Value("${jwt.expiration}")
          private?Long?expiration;

          根據(jù)用戶信息UserDetials生成Token

          /**
          ?*?根據(jù)用戶信息生成Token
          ?*?@param?userDetails
          ?*?@return
          ?*/

          public?String?generateToken(UserDetails?userDetails){
          ????//荷載
          ????Map?claim=new?HashMap<>();
          ????claim.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
          ????claim.put(CLAIM_KEY_CREATED,new?Date());
          ????return?generateToken(claim);
          }

          /**
          ?*?根據(jù)負載生成JWT?Token
          ?*?@param?claims
          ?*?@return
          ?*/

          private?String?generateToken(Map?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);
          }
          • 根據(jù)Token生成用戶名
          /**
          ?*?根據(jù)Token生成用戶名
          ?*?@param?token
          ?*?@return
          ?*/

          public?String?getUsernameFormToken(String?token){
          ????String?username;
          ????//根據(jù)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;
          }
          • 判斷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();
          }
          • 判斷Token是否可以被刷新
          /**
          ?*?判斷token是否可用被刷新
          ?*?如果已經(jīng)過期了,則可用被刷新,未過期,則不可用被刷新
          ?*?@param?token
          ?*?@return
          ?*/

          public?boolean?canRefresh(String?token){
          ????return?!isTokenExpired(token);
          }
          • 刷新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);
          }
          <2>登錄功能的實現(xiàn)

          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);
          }

          Service層

          /**
          ?*?登錄之后返回token
          ?*?@param?username
          ?*?@param?password
          ?*?@param?request
          ?*?@return
          ?*/

          @Override
          public?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在數(shù)據(jù)庫查出這個對象
          ????//在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("該賬號已經(jīng)被禁用,請聯(lián)系管理員");
          ????}

          ????//更新登錄用戶對象,放入security全局中,密碼不放
          ????UsernamePasswordAuthenticationToken?authenticationToken=new?UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
          ????SecurityContextHolder.getContext().setAuthentication(authenticationToken);

          ????//生成token
          ????String?token?=?jwtTokenUtil.generateToken(userDetails);
          ????Map?tokenMap=new?HashMap<>();
          ????tokenMap.put("token",token);
          ????tokenMap.put("tokenHead",tokenHead);//tokenHead,從配置文件yml中拿到的token的請求頭?==?Authorization
          ????return?RespBean.success("登陸成功",tokenMap);//將Token返回
          }
          <3>退出登錄

          退出登錄功能由前端實現(xiàn),我們只需要返回一個成功信息即可

          @ApiOperation(value?=?"退出登錄")
          @PostMapping("/logout")
          /**
          ?*?退出登錄
          ?*/

          public?RespBean?logout(){
          ????return?RespBean.success("注銷成功");
          }
          <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);
          ????????//不能返回前端用戶密碼,設(shè)置為空
          ????????admin.setPassword(null);
          ????????//將用戶角色返回
          ????????admin.setRoles(adminService.getRoles(admin.getId()));
          ????????return?admin;
          ????}
          <5>SpringSecurity的配置類SecurityConfig

          覆蓋SpringSecurity默認生成的賬號密碼,并讓他走我們自定義的登錄邏輯

          //讓SpringSecurity走我們自己登陸的UserDetailsService邏輯

          //認證信息的管理?用戶的存儲?這里配置的用戶信息會覆蓋掉SpringSecurity默認生成的賬號密碼
          @Override
          protected?void?configure(AuthenticationManagerBuilder?auth)?throws?Exception?{
          ????auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
          }
          //密碼加解密
          @Bean
          public?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("用戶名或密碼錯誤");
          ????};
          }

          登錄功能中使用的userDetailsService對象由這里注入,重寫loadUserByUsername方法實現(xiàn)自定義登錄邏輯。

          進行資源的攔截,權(quán)限設(shè)置,登錄過濾器設(shè)置。

          @Override
          protected?void?configure(HttpSecurity?http)?throws?Exception?{
          ????//使用Jwt不需要csrf
          ????http.csrf().disable()
          ????????????//基于token,不需要Session
          ????????????.sessionManagement()
          ????????????.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
          ????????????.and()
          ????????????//授權(quán)認證
          ????????????.authorizeRequests()
          ????????????.antMatchers("/doc.html").permitAll()
          ????????????//除了上面,所有的請求都要認證
          ????????????.anyRequest()
          ????????????.authenticated()
          ????????????.withObjectPostProcessor(new?ObjectPostProcessor()?{
          ????????????????//動態(tài)權(quán)限配置
          ????????????????@Override
          ????????????????public??O?postProcess(O?o)?{
          ????????????????????o.setAccessDecisionManager(customUrlDecisionManager);
          ????????????????????o.setSecurityMetadataSource(customFilter);
          ????????????????????return?o;
          ????????????????}
          ????????????})
          ????????????.and()
          ????????????//禁用緩存
          ????????????.headers()
          ????????????.cacheControl();

          ????//添加jwt登錄授權(quán)過濾器??判斷是否登錄
          ????http.addFilterBefore(jwtAuthencationTokenFilter(),?UsernamePasswordAuthenticationFilter.class);
          ????//添加自定義未授權(quán)和未登錄結(jié)果返回
          ????http.exceptionHandling()
          ????????//權(quán)限不足
          ????????????.accessDeniedHandler(restfulAccessDeniedHandler)
          ????????//未登錄
          ????????????.authenticationEntryPoint(restAuthorizationEntryPoint);

          }

          //將登錄過濾器注入
          @Bean
          public?JwtAuthencationTokenFilter?jwtAuthencationTokenFilter(){
          ????return?new?JwtAuthencationTokenFilter();
          }

          //需要放行的資源
          @Override
          public?void?configure(WebSecurity?web)?throws?Exception?{
          ????web.ignoring().antMatchers(
          ????????????"/login",
          ????????????"/logout",
          ????????????"/css/**",
          ????????????"/js/**",
          ????????????//首頁
          ????????????"/index.html",
          ????????????//網(wǎng)頁圖標
          ????????????"favicon.ico",
          ????????????//Swagger2
          ????????????"/doc.html",
          ????????????"/webjars/**",
          ????????????"/swagger-resources/**",
          ????????????"/v2/api-docs/**",
          ????????????//放行圖像驗證碼
          ????????????"/captcha",
          ????????????//WebSocket
          ????????????"/ws/**"
          ????);
          }

          登錄過濾器的配置

          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是否有效,重新設(shè)置用戶對象
          ????????????????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);
          ????}
          }

          添加未登錄結(jié)果處理器

          當未登錄或者Token失效時訪問未放行的接口時,自定義返回的結(jié)果

          @Component
          public?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();
          ????}
          }

          添加權(quán)限不足結(jié)果處理器

          當訪問接口沒有權(quán)限時,自定義返回結(jié)果

          @Component
          public?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("權(quán)限不足,請聯(lián)系管理員");
          ????????bean.setCode(401);
          ????????out.write(new?ObjectMapper().writeValueAsString(bean));
          ????????out.flush();
          ????????out.close();
          ????}
          }

          添加權(quán)限控制器,根據(jù)請求的URL確定訪問該URL需要什么角色

          @Component
          public?class?CustomFilter?implements?FilterInvocationSecurityMetadataSource?{

          ????@Autowired
          ????private?IMenuService?menuService;

          ????AntPathMatcher?antPathMatcher=new?AntPathMatcher();

          ????@Override
          ????public?Collection?getAttributes(Object?o)?throws?IllegalArgumentException?{
          ????????//獲取請求的URL
          ????????String?requestUrl?=?((FilterInvocation)?o).getRequestUrl();
          ????????List
          ?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?getAllConfigAttributes()?{
          ????????return?null;
          ????}

          ????@Override
          ????public?boolean?supports(Class?aClass)?{
          ????????return?false;
          ????}
          }

          添加權(quán)限控制器,對角色信息進行處理,是否可用訪問URL

          @Component
          public?class?CustomUrlDecisionManager?implements?AccessDecisionManager?{
          ????@Autowired
          ????private?CustomFilter?customFilter;
          ????@Override
          ????public?void?decide(Authentication?authentication,?Object?o,?Collection?collection)?throws?AccessDeniedException,?InsufficientAuthenticationException?{
          ????????for?(ConfigAttribute?configAttribute:?collection)?{
          ????????????//?當前url所需要的角色
          ????????????List?list=?(List)?customFilter.getAttributes(o);
          ????????????String[]?needRoles=new?String[list.size()];
          ????????????for?(int?i?=?0;?i?????????????????needRoles[i]=list.get(i).getAttribute();
          ????????????}
          ????????????//判斷角色是否登錄即可訪問的角色,此角色在CustomFilter中設(shè)置

          ????????????for?(String?needRole:needRoles)?{
          ????????????????if?("ROLE_LOGIN".equals((needRole)))?{
          ????????????????????//判斷是否已經(jīng)登錄
          ????????????????????if(authentication?instanceof?AnonymousAuthenticationToken){
          ????????????????????????throw?new?AccessDeniedException("尚未登錄,請登錄");
          ????????????????????}else?{
          ????????????????????????return;
          ????????????????????}
          ????????????????}
          ????????????}
          ????????????//判斷用戶角色是否為url所需要的角色
          ????????????//得到用戶擁有的角色??這里在Admin類中已經(jīng)將用戶的角色放入了
          ????????????Collection?authorities?=?authentication.getAuthorities();
          ????????????for?(String?needRole:needRoles)?{
          ????????????????for?(GrantedAuthority?authority:?authorities)?{
          ????????????????????if(authority.getAuthority().equals(needRole)){
          ????????????????????????return;
          ????????????????????}
          ????????????????}
          ????????????}
          ????????????throw?new?AccessDeniedException("權(quán)限不足,請聯(lián)系管理員");
          ????????}
          ????}

          ????@Override
          ????public?boolean?supports(ConfigAttribute?configAttribute)?{
          ????????return?false;
          ????}

          ????@Override
          ????public?boolean?supports(Class?aClass)?{
          ????????return?false;
          ????}
          }
          <6>Swagger2的配置
          @Configuration
          @EnableSwagger2
          public?class?Swagger2Config?{

          ????@Bean
          ????public?Docket?createRestApi(){
          ????????return?new?Docket(DocumentationType.SWAGGER_2)
          ????????????????//基礎(chǔ)設(shè)置
          ????????????????.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","[email protected]"))
          ????????????????.version("1.0")
          ????????????????.build();

          ????}

          ????private?List?securitySchemes(){
          ????????//設(shè)置請求頭信息
          ????????List?result=new?ArrayList<>();
          ????????ApiKey?apiKey=new?ApiKey("Authorization",?"Authorization","Header");
          ????????result.add(apiKey);
          ????????return?result;
          ????}

          ????private?List?securityContexts(){
          ????????//設(shè)置需要登錄認證的路徑
          ????????List?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?defaultAuth()?{
          ????????List?result=new?ArrayList<>();
          ????????//設(shè)置范圍為全局
          ????????AuthorizationScope?authorizationScope=new?AuthorizationScope("global","accessEeverything");
          ????????AuthorizationScope[]authorizationScopes=new?AuthorizationScope[1];
          ????????authorizationScopes[0]=authorizationScope;
          ????????result.add((new?SecurityReference("Authorization",authorizationScopes)));//這里的Authorization和上文ApiKey第二個參數(shù)一致
          ????????return??result;
          ????}
          }

          注意:

          ?ApiKey?apiKey=new?ApiKey("Authorization",?"Authorization","Header");
          <7>驗證碼功能(這里使用谷歌的驗證碼Captcha)

          驗證碼的配置類

          @Component
          public?class?CaptchaConfig?{
          ????@Bean
          ????public?DefaultKaptcha?defaultKaptcha(){
          ????????//驗證碼生成器
          ????????DefaultKaptcha?defaultKaptcha=new?DefaultKaptcha();
          ????????//配置
          ????????Properties?properties?=?new?Properties();
          ????????//是否有邊框
          ????????properties.setProperty("kaptcha.border",?"yes");
          ????????//設(shè)置邊框顏色
          ????????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");
          ????????//設(shè)置字體樣式
          ????????properties.setProperty("kaptcha.textproducer.font.names",?"宋體,楷體,微軟雅黑");
          ????????//字體大小,默認40
          ????????properties.setProperty("kaptcha.textproducer.font.size",?"30");
          ????????//驗證碼文本字符內(nèi)容范圍?默認為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;
          ????}
          }

          驗證碼的控制器

          @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?--------------------------
          ????????//獲取驗證碼文本內(nèi)容
          ????????String?text=defaultKaptcha.createText();
          ????????System.out.println("驗證碼內(nèi)容"+text);
          ????????//將驗證碼文本內(nèi)容放入Session
          ????????request.getSession().setAttribute("captcha",text);
          ????????//根據(jù)文本驗證碼內(nèi)容創(chuàng)建圖形驗證碼
          ????????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?--------------------------
          ????}
          }
          <8>根據(jù)用戶ID查詢用戶所擁有操控權(quán)限的菜單列表

          Controller層

          @ApiOperation(value?=?"通過用戶ID查詢菜單列表")
          @GetMapping("/menu")
          public?List
          ?getMenuByAdminId(){
          ????return?menuService.getMenuByAdminId();
          }

          Service層

          @Override
          public?List
          ?getMenuByAdminId()?{
          ????//從Security全局上下文中獲取當前登錄用戶Admin
          ????Admin?admin=?AdminUtil.getCurrentAdmin();
          ????Integer?adminId=admin.getId();
          ????ValueOperations?valueOperations?=?redisTemplate.opsForValue();
          ????//從Redis獲取菜單數(shù)據(jù)
          ????List
          ?menus?=?(List)?valueOperations.get("menu_"?+?adminId);

          ????//如果為空,從數(shù)據(jù)庫中獲取
          ????if(CollectionUtils.isEmpty(menus)){
          ????????menus=menuMapper.getMenuByAdminId(adminId);
          ????????//查詢之后放入Redis
          ????????valueOperations.set("menu_"+adminId,menus);
          ????}
          ????return?menus;
          }

          Mapper層

          --?根據(jù)用戶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>
          <9>使用Redis緩存根據(jù)用戶ID查出來的菜單信息

          Redis的配置類

          @Configuration
          public?class?RedisConfig?{
          ????@Bean
          ????public?RedisTemplate?redisTemplate(RedisConnectionFactory?redisConnectionFactory){
          ????????RedisTemplate?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;
          ????}
          }
          <10>全局異常的統(tǒng)一處理
          @RestControllerAdvice
          public?class?GlobalException?{
          ????@ExceptionHandler(SQLException.class)
          ????public?RespBean?respBeanMysqlException(SQLException?e)
          {
          ????????if(e?instanceof?SQLIntegrityConstraintViolationException){
          ????????????return?RespBean.error("該數(shù)據(jù)有關(guān)聯(lián)數(shù)據(jù),操作失敗");
          ????????}
          ????????e.printStackTrace();
          ????????return?RespBean.error("數(shù)據(jù)庫異常,操作失敗");
          ????}

          ????@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("未知錯誤,請聯(lián)系管理員");
          ????}
          }

          3.基礎(chǔ)信息設(shè)置模塊

          職位,職稱,權(quán)限組管理僅涉及單表的增刪查改,這里不多寫

          <1>部門管理

          獲取所有部門

          Mapper層:涉及父子類,遞歸查找

          "getAllDepartments"?resultMap="DepartmentWithChildren">
          ????select
          ????"Base_Column_List"/>
          ????from?t_department
          ????where?parentId=#{parentId}



          ????"BaseResultMap"?type="org.example.server.pojo.Department">
          ????????"id"?property="id"?/>
          ????????"name"?property="name"?/>
          ????????"parentId"?property="parentId"?/>
          ????????"depPath"?property="depPath"?/>
          ????????"enabled"?property="enabled"?/>
          ????????"isParent"?property="isParent"?/>
          ????

          ????"DepartmentWithChildren"?type="org.example.server.pojo.Department"?extends="BaseResultMap">
          ????????"children"?ofType="org.example.server.pojo.Department"?select="org.example.server.mapper.DepartmentMapper.getAllDepartments"
          ????????column="id">
          ????????
          ????
          ????
          ????"Base_Column_List">
          ????????id,?name,?parentId,?depPath,?enabled,?isParent
          ????

          添加部門



          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})

          刪除部門



          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})

          4.薪資模塊及薪資管理模塊

          這里僅介紹獲取全部操作員及操作員角色的更新,其他功能都是單表簡單的增刪查改

          <1>獲取全部操作員

          Controller層

          @ApiOperation(value?=?"獲取所有操作員")
          @GetMapping("/")
          public?List?getAllAdmins(String?keywords){
          ????return?adminService.getAllAdmins(keywords);
          }

          Service層

          /**
          ?*?獲取所有操作員
          ?*?@param?keywords
          ?*/

          @Override
          public?List?getAllAdmins(String?keywords)?{
          ????//要傳當前登錄的Id,當前操作員不用查
          ????return?adminMapper.getAllAdmins(AdminUtil.getCurrentAdmin().getId(),keywords);
          }

          Mapper層


          "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

          涉及操作員角色的查詢

          <2>操作員角色的修改

          Service層:

          /**
          ?*?更新操作員角色
          ?*?@param?adminId
          ?*?@param?rids
          ?*?@return
          ?*/

          @Override
          @Transactional
          public?RespBean?updateAdminRole(Integer?adminId,?Integer[]?rids)?{
          ????//先將已經(jīng)擁有的角色全部刪除
          ????adminRoleMapper.delete(new?QueryWrapper().eq("adminId",adminId));
          ????//再將傳過來的所有角色添加
          ????Integer?result?=?adminRoleMapper.addAdminRole(adminId,?rids);
          ????if(result?==?rids.length){
          ????????return?RespBean.success("修改角色成功");
          ????}
          ????return?RespBean.error("更新角色失敗");
          }

          思想:先將操作員所有的角色都刪除,再將前端闖入的角色全部添加

          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);
          }

          Service層

          @Override
          public?RespPageBean?getEmployeeByPage(Integer?currentPage,?Integer?size,?Employee?employee,?LocalDate[]?beginDateScope)?{
          ????Page?page=new?Page<>(currentPage,size);
          ????IPage?iPage=employeeMapper.getEmployeeByPage(page,employee,beginDateScope);
          ????RespPageBean?respPageBean=new?RespPageBean();
          ????respPageBean.setTotal(iPage.getTotal());
          ????respPageBean.setData(iPage.getRecords());
          ????return?respPageBean;
          }

          Mapper層

          "EmployeeInfo"?type="org.example.server.pojo.Employee"?extends="BaseResultMap">
          ????????"nation"?javaType="org.example.server.pojo.Nation">
          ????????????"nid"?property="id"?/>
          ????????????"nname"?property="name"?/>
          ????????
          ????????"politicsStatus"?javaType="org.example.server.pojo.PoliticsStatus">
          ????????????"pid"?property="id"?/>
          ????????????"pname"?property="name"?/>
          ????????
          ????????"department"?javaType="org.example.server.pojo.Department">
          ????????????"did"?property="id"?/>
          ????????????"dname"?property="name"?/>
          ????????
          ????????"joblevel"?javaType="org.example.server.pojo.Joblevel">
          ????????????"jid"?property="id"?/>
          ????????????"jname"?property="name"?/>
          ????????
          ????????"position"?javaType="org.example.server.pojo.Position">
          ????????????"posid"?property="id"?/>
          ????????????"posname"?property="name"?/>
          ????????
          ????



          "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

          <2>使用EasyPOI對員工信息進行導入和導出

          EasyPOI注解的使用

          用于員工數(shù)據(jù)導入:Excel表中的部門,職稱等字段在數(shù)據(jù)庫員工表中找不到字段,數(shù)據(jù)庫中是以id外鍵字段存儲

          員工數(shù)據(jù)的導出

          @ApiOperation(value?=?"導出員工數(shù)據(jù)")
          @GetMapping(value?=?"/export",produces?=?"application/octet-stream")
          public?void?exportEmployee(HttpServletResponse?response){
          ????List?list?=?employeeService.getEmployee(null);
          ????//參數(shù):文件名,表名,導出的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();
          ????????????}
          ????????}
          ????}
          }

          員工數(shù)據(jù)的導入

          @ApiOperation(value?=?"導入員工數(shù)據(jù)")
          @PostMapping("/import")
          public?RespBean?importEmployee(MultipartFile?file){
          ????//準備導入的數(shù)據(jù)表
          ????ImportParams?params=new?ImportParams();
          ????//去掉第一行:標題行
          ????params.setTitleRows(1);
          ????List?nationList?=?nationService.list();
          ????List?politicsStatusList=politicsStatusService.list();
          ????List?departmentList=departmentService.list();
          ????List?joblevelList=joblevelService.list();
          ????List?positionList=positionService.list();
          ????try?{
          ????????//將Excel表變?yōu)長ist
          ????????List?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("導入失敗");
          }
          <3>使用RabbitMQ對新入職的員工發(fā)送歡迎郵件

          這里使用SMTP:需要先去郵箱開通SMTP服務

          • RabbitMQ消息發(fā)送的可靠性

          消息落庫,對消息狀態(tài)進行標記

          步驟:

          • 發(fā)送消息時,將當前消息數(shù)據(jù)存入數(shù)據(jù)庫,投遞狀態(tài)為消息投遞中
          • 開啟消息確認回調(diào)機制。確認成功,更新投遞狀態(tài)為消息投遞成功
          • 開啟定時任務,重新投遞失敗的消息。重試超過3次,更新投遞狀態(tài)為投遞失敗

          消息延遲投遞,做二次確認,回調(diào)檢查

          步驟:

          • 發(fā)送消息時,將當前消息存入數(shù)據(jù)庫,消息狀態(tài)為消息投遞

          • 過一段時間進行第二次的消息發(fā)送

          • 開啟消息回調(diào)機制,當?shù)谝淮伟l(fā)送的消息被成功消費時,消費端的確認會被MQ Broker監(jiān)聽,成功則將消息隊列中的狀態(tài)變?yōu)橥哆f成功

          • 如果消息投遞沒有成功,則過一段時間第二次發(fā)送的消息也會被MQ Broker監(jiān)聽到,會根據(jù)這條消息的ID去消息數(shù)據(jù)庫查找,如果發(fā)現(xiàn)消息數(shù)據(jù)庫中的狀態(tài)為投遞中而不是投遞成功,則會通知消息放松端重新進行步驟一

          • 消息功能的實現(xiàn)

          在進行新員工插入成功后,對新員工發(fā)出郵件,并將發(fā)送的郵件保存到數(shù)據(jù)庫中

          ????//獲取合同開始和結(jié)束的時間
          ????LocalDate?beginContact=employee.getBeginContract();
          ????LocalDate?endContact=employee.getEndContract();
          ????long?days?=?beginContact.until(endContact,?ChronoUnit.DAYS);
          ????//保留兩位小數(shù)
          ????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);
          ????????//數(shù)據(jù)庫記錄發(fā)送的消息
          ????????String?msgId?=?UUID.randomUUID().toString();
          ????????MailLog?mailLog=new?MailLog();
          ????????mailLog.setMsgId(msgId);
          ????????mailLog.setEid(employee.getId());
          ????????mailLog.setStatus(0);
          ????????//消息的狀態(tài)保存在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);

          ????????//發(fā)送信息
          ????????//發(fā)送交換機,路由鍵,用戶對象和消息ID
          ????????rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,
          ????????????????MailConstants.MAIL_ROUTING_KEY_NAME,
          ????????????????emp,
          ????????????????new?CorrelationData(msgId));
          ????????return?RespBean.success("添加成功");
          ????}
          ????return?RespBean.error("添加失敗");
          }

          消費端的處理,這里我們使用上述第一種方式,—>消息落庫,對消息狀態(tài)進行標記. 為保證消費者不重復消費同一消息,采取 消息序號+我們傳入的消息msgId來識別每一個消息

          @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)
          ????//拿取Message?和?channel?可以拿到?消息序號鑒別消息是否統(tǒng)一個消息多收????通過消息序號+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中拿取,如果存在,說明消息已經(jīng)發(fā)送成功了,這里直接確認返回
          ????????????if?(hashOperations.entries("mail_log").containsKey(msgId)){
          ????????????????LOGGER.error("消息已經(jīng)被消費=============>{}",msgId);
          ????????????????/**
          ?????????????????*?手動確認消息
          ?????????????????*?tag:消息序號
          ?????????????????*?multiple:是否確認多條
          ?????????????????*/

          ????????????????channel.basicAck(tag,false);
          ????????????????return;
          ????????????}
          ????????????MimeMessage?msg?=?javaMailSender.createMimeMessage();
          ????????????MimeMessageHelper?helper?=?new?MimeMessageHelper(msg);
          ????????????//發(fā)件人
          ????????????helper.setFrom(mailProperties.getUsername());
          ????????????//收件人
          ????????????helper.setTo(employee.getEmail());
          ????????????//主題
          ????????????helper.setSubject("入職歡迎郵件");
          ????????????//發(fā)送日期
          ????????????helper.setSentDate(new?Date());
          ????????????//郵件內(nèi)容
          ????????????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模板中的信息轉(zhuǎn)為String
          ????????????String?mail?=?templateEngine.process("mail",?context);
          ????????????helper.setText(mail,?true);
          ????????????//發(fā)送郵件
          ????????????javaMailSender.send(msg);
          ????????????LOGGER.info("郵件發(fā)送成功");
          ????????????//將消息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("郵件發(fā)送失敗=========>{}",?e.getMessage());
          ????????????}
          ????????????LOGGER.error("郵件發(fā)送失敗=========>{}",?e.getMessage());
          ????????}
          ????}
          }

          消息的配置類,確認應答等

          @Configuration
          public?class?RabbitMQConfig?{
          ????private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(RabbitMQConfig.class);
          ????@Autowired
          ????private?CachingConnectionFactory?cachingConnectionFactory;

          ????@Autowired
          ????private?IMailLogService?mailLogService;

          ????@Bean
          ????public?RabbitTemplate?rabbitTemplate(){
          ????????RabbitTemplate?rabbitTemplate?=?new?RabbitTemplate(cachingConnectionFactory);

          ????????/**
          ?????????*?消息確認回調(diào),確認消息是否到達broker
          ?????????*?data:消息的唯一標識
          ?????????*?ack:確認結(jié)果
          ?????????*?cause:失敗原因
          ?????????*/

          ????????rabbitTemplate.setConfirmCallback((data,ack,cause)->{
          ????????????String?msgId?=?data.getId();
          ????????????if(ack){
          ????????????????LOGGER.info("{}======>消息發(fā)送成功",msgId);
          ????????????????mailLogService.update(new?UpdateWrapper().set("status",1?).eq("msgId",msgId));
          ????????????}else?{
          ????????????????LOGGER.error("{}=====>消息發(fā)送失敗",msgId);
          ????????????}
          ????????});

          ????????/**
          ?????????*?消息失敗回調(diào),比如router不到queue時回調(diào)
          ?????????*?msg:消息的主題
          ?????????*?repCode:響應碼
          ?????????*?repText:響應描述
          ?????????*?exchange:交換機
          ?????????*?routingkey:路由鍵
          ?????????*/

          ????????rabbitTemplate.setReturnCallback((msg,repCode,repText,exchange,routingkey)->{
          ????????????LOGGER.error("{}=====>消息發(fā)送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);
          ????}

          6.在線聊天功能的實現(xiàn)

          這里使用WebSocket

          WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協(xié)議。

          WebSocket 使得客戶端和服務器之間的數(shù)據(jù)交換變得更加簡單,允許服務端主動向客戶端推送數(shù)據(jù)。

          在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進行雙向數(shù)據(jù)傳輸。

          它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發(fā)送信息,是真正的雙向平等對話,屬于服務器推送技術(shù)的一種。

          WebSocket的配置

          這里主要是前端實現(xiàn),后端只是增加一些配置

          @Configuration
          @EnableWebSocketMessageBroker
          public?class?WebSocketConfig?implements?WebSocketMessageBrokerConfigurer?{
          ????@Value("${jwt.tokenHead}")
          ????private?String?tokenHead;
          ????@Autowired
          ????private?JwtTokenUtil?jwtTokenUtil;
          ????@Autowired
          ????private?UserDetailsService?userDetailsService;


          ????/**
          ?????*?添加這個Endpoint,這樣在網(wǎng)頁可以通過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();
          ????}


          ????/**
          ?????*?輸入通道參數(shù)配置??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,并且設(shè)置用戶對象
          ????????????????if?(StompCommand.CONNECT.equals(accessor.getCommand())){
          ????????????????????//拿取Token
          ????????????????????String?token?=?accessor.getFirstNativeHeader("Auth-Token");//參數(shù)前端已經(jīng)固定
          ????????????????????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是否有效,重新設(shè)置用戶對象
          ????????????????????????????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");
          ????}
          }
          作者:Serendipity ?sn 鏈接:
          blog.csdn.net/qq_45704528/article/details/119699269

          怎么接私活?這個渠道你100%有用!請收藏!


          ,在看?
          瀏覽 89
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  美女网黄射 | 午夜成人精品偷拍在线 | 国产一级AA片 | 国产精品美女久久久久久久久 | 狠狠干99 |