基于 SpringBoot + MyBatis 前后端分離實現(xiàn)的在線辦公系統(tǒng)
點擊關(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添加權(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?extends?GrantedAuthority>?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{
????return?menuService.getMenuByAdminId();
}
Service層
@Override
public?List{
????//從Security全局上下文中獲取當前登錄用戶Admin
????Admin?admin=?AdminUtil.getCurrentAdmin();
????Integer?adminId=admin.getId();
????ValueOperations?valueOperations?=?redisTemplate.opsForValue();
????//從Redis獲取菜單數(shù)據(jù)
????List 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層:涉及父子類,遞歸查找
????"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
????
添加部門
刪除部門
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層
涉及操作員角色的查詢
<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"?/>
????????
????
<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%有用!請收藏!
喜歡文章,點個在看?












