0


基于SpringBoot + MyBatis的前后端分离实现在线办公系统

在线办公系统

目录

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

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

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

使用验证码登录

在这里插入图片描述

页面展示:

在这里插入图片描述

  1. 添加依赖

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

2.登录模块及配置框架搭建

<1>Jwt工具类及对Token的处理

1.1根据用户信息生成Token

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

//用户名的keyprivatestaticfinalString CLAIM_KEY_USERNAME="sub";//签名的时间privatestaticfinalString CLAIM_KEY_CREATED="created";
  1. 从配置文件中拿到Jwt的密钥和失效时间

/**
 * @Value的值有两类:
 * ① ${ property : default_value }
 * ② #{ obj.property? :default_value }
 * 第一个注入的是外部配置文件对应的property,第二个则是SpEL表达式对应的内容。 那个
 * default_value,就是前面的值为空时的默认值。注意二者的不同,#{}里面那个obj代表对象。
 *///JWT密钥@Value("${jwt.secret}")privateString secret;//JWT失效时间@Value("${jwt.expiration}")privateLong expiration;
  1. 根据用户信息UserDetials生成Token

/**
 * 根据用户信息生成Token
 * @param userDetails
 * @return
 */publicStringgenerateToken(UserDetails userDetails){//荷载Map<String,Object> claim=newHashMap<>();
    claim.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
    claim.put(CLAIM_KEY_CREATED,newDate());returngenerateToken(claim);}/**
 * 根据负载生成JWT Token
 * @param claims
 * @return
 */privateStringgenerateToken(Map<String,Object> claims){returnJwts.builder().setClaims(claims).setExpiration(generateExpirationDate())//添加失效时间.signWith(SignatureAlgorithm.HS512,secret)//添加密钥以及加密方式.compact();}/**
 * 生成Token失效时间  当前时间+配置的失效时间
 * @return
 */privateDategenerateExpirationDate(){returnnewDate(System.currentTimeMillis()+expiration*1000);}

1.2根据Token生成用户名

/**
 * 根据Token生成用户名
 * @param token
 * @return
 */publicStringgetUsernameFormToken(String token){String username;//根据Token去拿荷载try{Claims claim=getClaimFromToken(token);
        username=claim.getSubject();//获取用户名}catch(Exception e){
        e.printStackTrace();
        username=null;}return username;}/**
 * 从Token中获取荷载
 * @param token
 * @return
 */privateClaimsgetClaimFromToken(String token){Claims claims=null;try{
        claims=Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}catch(Exception e){
        e.printStackTrace();}return claims;}

1.3判断Token是否有效

/**
 * 判断Token是否有效
 * Token是否过期
 * Token中的username和UserDetails中的username是否一致
 * @param token
 * @param userDetails
 * @return
 */publicbooleanTokenIsValid(String token,UserDetails userDetails){String username =getUsernameFormToken(token);return username.equals(userDetails.getUsername())&&!isTokenExpired(token);}/**
 * 判断Token是否过期
 * @param token
 * @return
 */privatebooleanisTokenExpired(String token){//获取Token的失效时间Date expireDate=getExpiredDateFromToken(token);//在当前时间之前,则失效return expireDate.before(newDate());}/**
 * 获取Token的失效时间
 * @param token
 * @return
 */privateDategetExpiredDateFromToken(String token){Claims claims =getClaimFromToken(token);return claims.getExpiration();}

1.4判断Token是否可以被刷新

/**
 * 判断token是否可用被刷新
 * 如果已经过期了,则可用被刷新,未过期,则不可用被刷新
 * @param token
 * @return
 */publicbooleancanRefresh(String token){return!isTokenExpired(token);}

1.5刷新Token,获取新的Token

/**
 * 刷新Token
 * @param token
 * @return
 */publicStringrefreshToken(String token){Claims claims=getClaimFromToken(token);
    claims.put(CLAIM_KEY_CREATED,newDate());returngenerateToken(claims);}

<2>登录功能的实现

  • Controller层@ApiOperation(value ="登录之后返回token")@PostMapping("/login")//AdminLoginParam 自定义登录时传入的对象,包含账号,密码,验证码 publicRespBeanlogin(@RequestBodyAdminLoginParam adminLoginParam,HttpServletRequest request){return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request);}
  • Service层/** * 登录之后返回token * @param username * @param password * @param request * @return */@OverridepublicRespBeanlogin(String username,String password,String code,HttpServletRequest request){String captcha =(String)request.getSession().getAttribute("captcha");//验证码功能,后面提到//验证码为空或匹配不上if((code ==null|| code.length()==0)||!captcha.equalsIgnoreCase(code)){returnRespBean.error("验证码错误,请重新输入");}//通过username在数据库查出这个对象//在SecurityConfig配置文件中,重写了loadUserByUsername方法,返回了userDetailsService Bean对象,使用我们自己的登录逻辑UserDetails userDetails = userDetailsService.loadUserByUsername(username);//如果userDetails为空或userDetails中的密码和传入的密码不相同if(userDetails ==null||!passwordEncoder.matches(password,userDetails.getPassword())){returnRespBean.error("用户名或密码不正确");}//判断账号是否可用if(!userDetails.isEnabled()){returnRespBean.error("该账号已经被禁用,请联系管理员");}//更新登录用户对象,放入security全局中,密码不放UsernamePasswordAuthenticationToken authenticationToken=newUsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);//生成tokenString token = jwtTokenUtil.generateToken(userDetails);Map<String,String> tokenMap=newHashMap<>(); tokenMap.put("token",token); tokenMap.put("tokenHead",tokenHead);//tokenHead,从配置文件yml中拿到的token的请求头 == AuthorizationreturnRespBean.success("登陆成功",tokenMap);//将Token返回}

<3>退出登录

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

@ApiOperation(value ="退出登录")@PostMapping("/logout")/**
 * 退出登录
 */publicRespBeanlogout(){returnRespBean.success("注销成功");}

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

  • Controller层@ApiOperation(value ="获取当前登录用户的信息")@GetMapping("/admin/info")publicAdmingetAdminInfo(Principal principal){//可通过principal对象获取当前登录对象if(principal ==null){returnnull;}//当前用户的用户名String username = principal.getName();Admin admin= adminService.getAdminByUsername(username);//不能返回前端用户密码,设置为空 admin.setPassword(null);//将用户角色返回 admin.setRoles(adminService.getRoles(admin.getId()));return admin;}

<5>SpringSecurity的配置类SecurityConfig

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

//让SpringSecurity走我们自己登陆的UserDetailsService逻辑//认证信息的管理 用户的存储 这里配置的用户信息会覆盖掉SpringSecurity默认生成的账号密码@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
    auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());}//密码加解密@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@Override@Bean//注入到IOC中,在登录时使用到的userDetailsService就是这个Bean,loadUserByUsername方法是这里重写过的publicUserDetailsServiceuserDetailsService(){return username->{Admin admin=adminService.getAdminByUsername(username);if(admin !=null){
            admin.setRoles(adminService.getRoles(admin.getId()));return admin;}thrownewUsernameNotFoundException("用户名或密码错误");};}

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

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

@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{//使用Jwt不需要csrf
    http.csrf().disable()//基于token,不需要Session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//授权认证.authorizeRequests().antMatchers("/doc.html").permitAll()//除了上面,所有的请求都要认证.anyRequest().authenticated().withObjectPostProcessor(newObjectPostProcessor<FilterSecurityInterceptor>(){//动态权限配置@Overridepublic<OextendsFilterSecurityInterceptor>OpostProcess(O o){
                    o.setAccessDecisionManager(customUrlDecisionManager);
                    o.setSecurityMetadataSource(customFilter);return o;}}).and()//禁用缓存.headers().cacheControl();//添加jwt登录授权过滤器  判断是否登录
    http.addFilterBefore(jwtAuthencationTokenFilter(),UsernamePasswordAuthenticationFilter.class);//添加自定义未授权和未登录结果返回
    http.exceptionHandling()//权限不足.accessDeniedHandler(restfulAccessDeniedHandler)//未登录.authenticationEntryPoint(restAuthorizationEntryPoint);}//将登录过滤器注入@BeanpublicJwtAuthencationTokenFilterjwtAuthencationTokenFilter(){returnnewJwtAuthencationTokenFilter();}//需要放行的资源@Overridepublicvoidconfigure(WebSecurity web)throwsException{
    web.ignoring().antMatchers("/login","/logout","/css/**","/js/**",//首页"/index.html",//网页图标"favicon.ico",//Swagger2"/doc.html","/webjars/**","/swagger-resources/**","/v2/api-docs/**",//放行图像验证码"/captcha",//WebSocket"/ws/**");}
5.2.1登录过滤器的配置
publicclassJwtAuthencationTokenFilterextendsOncePerRequestFilter{//Jwt存储头@Value("${jwt.tokenHeader}")privateString tokenHeader;//Jwt头部信息@Value("${jwt.tokenHead}")privateString tokenHead;@AutowiredprivateJwtTokenUtil jwtTokenUtil;@AutowiredprivateUserDetailsService userDetailsService;@OverrideprotectedvoiddoFilterInternal(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,FilterChain filterChain)throwsServletException,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)){//截取真正的jwtString authToken=authHeader.substring(tokenHead.length());String username=jwtTokenUtil.getUsernameFormToken(authToken);//token存在用户名但是未登录if(username !=null&&SecurityContextHolder.getContext().getAuthentication()==null){//登录UserDetails userDetails = userDetailsService.loadUserByUsername(username);//验证token是否有效,重新设置用户对象if(jwtTokenUtil.TokenIsValid(authToken,userDetails)){//把对象放到Security的全局中UsernamePasswordAuthenticationToken authenticationToken=newUsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());//将请求中的Session等信息放入Details,再放入Security全局中
                authenticationToken.setDetails(newWebAuthenticationDetailsSource().buildDetails(httpServletRequest));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}}}//放行
        filterChain.doFilter(httpServletRequest,httpServletResponse);}}
5.2.2添加未登录结果处理器

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

@ComponentpublicclassRestAuthorizationEntryPointimplementsAuthenticationEntryPoint{@Overridepublicvoidcommence(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,AuthenticationException e)throwsIOException,ServletException{
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");PrintWriter out = httpServletResponse.getWriter();RespBean bean=RespBean.error("尚未登录,请登录");
        bean.setCode(401);
        out.write(newObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();}}
5.2.3添加权限不足结果处理器

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

@ComponentpublicclassRestfulAccessDeniedHandlerimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,AccessDeniedException e)throwsIOException,ServletException{
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");PrintWriter out = httpServletResponse.getWriter();RespBean bean=RespBean.success("权限不足,请联系管理员");
        bean.setCode(401);
        out.write(newObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();}}
5.2.4添加权限控制器,根据请求的URL确定访问该URL需要什么角色
@ComponentpublicclassCustomFilterimplementsFilterInvocationSecurityMetadataSource{@AutowiredprivateIMenuService menuService;AntPathMatcher antPathMatcher=newAntPathMatcher();@OverridepublicCollection<ConfigAttribute>getAttributes(Object o)throwsIllegalArgumentException{//获取请求的URLString requestUrl =((FilterInvocation) o).getRequestUrl();List<Menu> menus = menuService.getMenuWithRole();//将URL所需要的角色放入Menu中for(Menu menu:menus){//判断请求Url与菜单角色拥有的url是否匹配if(antPathMatcher.match(menu.getUrl(),requestUrl)){// 该Url所需要的角色String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);//如果匹配上放入配置中,需要的角色returnSecurityConfig.createList(str);}}//没匹配的url默认登录即可访问returnSecurityConfig.createList("ROLE_LOGIN");}@OverridepublicCollection<ConfigAttribute>getAllConfigAttributes(){returnnull;}@Overridepublicbooleansupports(Class<?> aClass){returnfalse;}}
5.2.5添加权限控制器,对角色信息进行处理,是否可用访问URL
@ComponentpublicclassCustomUrlDecisionManagerimplementsAccessDecisionManager{@AutowiredprivateCustomFilter customFilter;@Overridepublicvoiddecide(Authentication authentication,Object o,Collection<ConfigAttribute> collection)throwsAccessDeniedException,InsufficientAuthenticationException{for(ConfigAttribute configAttribute: collection){// 当前url所需要的角色List<ConfigAttribute> list=(List<ConfigAttribute>) customFilter.getAttributes(o);String[] needRoles=newString[list.size()];for(int i =0; i <list.size(); i++){
                needRoles[i]=list.get(i).getAttribute();}//判断角色是否登录即可访问的角色,此角色在CustomFilter中设置for(String needRole:needRoles){if("ROLE_LOGIN".equals((needRole))){//判断是否已经登录if(authentication instanceofAnonymousAuthenticationToken){thrownewAccessDeniedException("尚未登录,请登录");}else{return;}}}//判断用户角色是否为url所需要的角色//得到用户拥有的角色  这里在Admin类中已经将用户的角色放入了Collection<?extendsGrantedAuthority> authorities = authentication.getAuthorities();for(String needRole:needRoles){for(GrantedAuthority authority: authorities){if(authority.getAuthority().equals(needRole)){return;}}}thrownewAccessDeniedException("权限不足,请联系管理员");}}@Overridepublicbooleansupports(ConfigAttribute configAttribute){returnfalse;}@Overridepublicbooleansupports(Class<?> aClass){returnfalse;}}

<6>Swagger2的配置

@Configuration@EnableSwagger2publicclassSwagger2Config{@BeanpublicDocketcreateRestApi(){returnnewDocket(DocumentationType.SWAGGER_2)//基础设置.apiInfo(apiInfo())//扫描哪个包.select().apis(RequestHandlerSelectors.basePackage("org.example.server.controller"))//任何路径都可以.paths(PathSelectors.any()).build().securityContexts(securityContexts()).securitySchemes(securitySchemes());}privateApiInfoapiInfo(){returnnewApiInfoBuilder().title("云E办接口文档").description("云E办接口文档").contact(newContact("朱云飞","http:localhost:8081/doc.html","[email protected]")).version("1.0").build();}privateList<ApiKey>securitySchemes(){//设置请求头信息List<ApiKey> result=newArrayList<>();ApiKey apiKey=newApiKey("Authorization","Authorization","Header");
        result.add(apiKey);return result;}privateList<SecurityContext>securityContexts(){//设置需要登录认证的路径List<SecurityContext> result=newArrayList<>();
        result.add(getContextByPath("/hello/.*"));return result;}privateSecurityContextgetContextByPath(String pathRegex){returnSecurityContext.builder().securityReferences(defaultAuth())//添加全局认证.forPaths(PathSelectors.regex(pathRegex))//带有pathRegex字段的接口访问不带添加的Authorization全局变量.build();}//添加Swagger全局的Authorization  全局认证    固定的代码privateList<SecurityReference>defaultAuth(){List<SecurityReference> result=newArrayList<>();//设置范围为全局AuthorizationScope authorizationScope=newAuthorizationScope("global","accessEeverything");AuthorizationScope[]authorizationScopes=newAuthorizationScope[1];
        authorizationScopes[0]=authorizationScope;
        result.add((newSecurityReference("Authorization",authorizationScopes)));//这里的Authorization和上文ApiKey第二个参数一致return  result;}}

注意:

ApiKey apiKey=newApiKey("Authorization","Authorization","Header");

在这里插入图片描述

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

7.1验证码的配置类

@ComponentpublicclassCaptchaConfig{@BeanpublicDefaultKaptchadefaultKaptcha(){//验证码生成器DefaultKaptcha defaultKaptcha=newDefaultKaptcha();//配置Properties properties =newProperties();//是否有边框
        properties.setProperty("kaptcha.border","yes");//设置边框颜色
        properties.setProperty("kaptcha.border.color","105,179,90");//边框粗细度,默认为1// properties.setProperty("kaptcha.border.thickness","1");//验证码
        properties.setProperty("kaptcha.session.key","code");//验证码文本字符颜色 默认为黑色
        properties.setProperty("kaptcha.textproducer.font.color","blue");//设置字体样式
        properties.setProperty("kaptcha.textproducer.font.names","宋体,楷体,微软雅黑");//字体大小,默认40
        properties.setProperty("kaptcha.textproducer.font.size","30");//验证码文本字符内容范围 默认为abced2345678gfynmnpwx// properties.setProperty("kaptcha.textproducer.char.string", "");//字符长度,默认为5
        properties.setProperty("kaptcha.textproducer.char.length","4");//字符间距 默认为2
        properties.setProperty("kaptcha.textproducer.char.space","4");//验证码图片宽度 默认为200
        properties.setProperty("kaptcha.image.width","100");//验证码图片高度 默认为40
        properties.setProperty("kaptcha.image.height","40");Config config =newConfig(properties);
        defaultKaptcha.setConfig(config);return defaultKaptcha;}}

7.2验证码的控制器

@RestControllerpublicclassCaptchaController{@AutowiredprivateDefaultKaptcha defaultKaptcha;@ApiOperation(value ="验证码")@GetMapping(value ="/captcha",produces ="image/jpeg")publicvoidcaptcha(HttpServletRequest request,HttpServletResponse response){// 定义response输出类型为image/jpeg类型
        response.setDateHeader("Expires",0);// Set standard HTTP/1.1 no-cache headers.
        response.setHeader("Cache-Control","no-store, no-cache, must-revalidate");// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
        response.addHeader("Cache-Control","post-check=0, pre-check=0");// Set standard HTTP/1.0 no-cache header.
        response.setHeader("Pragma","no-cache");// return a jpeg
        response.setContentType("image/jpeg");//-------------------生成验证码 begin --------------------------//获取验证码文本内容String text=defaultKaptcha.createText();System.out.println("验证码内容"+text);//将验证码文本内容放入Session
        request.getSession().setAttribute("captcha",text);//根据文本验证码内容创建图形验证码BufferedImage image = defaultKaptcha.createImage(text);ServletOutputStream outputStream=null;try{
             outputStream = response.getOutputStream();//输出流输出图片,格式为jpgImageIO.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>根据用户ID查询用户所拥有操控权限的菜单列表

在这里插入图片描述

  • Controller层@ApiOperation(value ="通过用户ID查询菜单列表")@GetMapping("/menu")publicList<Menu>getMenuByAdminId(){return menuService.getMenuByAdminId();}
  • Service层@OverridepublicList<Menu>getMenuByAdminId(){//从Security全局上下文中获取当前登录用户AdminAdmin admin=AdminUtil.getCurrentAdmin();Integer adminId=admin.getId();ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue();//从Redis获取菜单数据List<Menu> menus =(List<Menu>) valueOperations.get("menu_"+ adminId);//如果为空,从数据库中获取if(CollectionUtils.isEmpty(menus)){ menus=menuMapper.getMenuByAdminId(adminId);//查询之后放入Redis valueOperations.set("menu_"+adminId,menus);}return menus;}
  • Mapper层<!-- 根据用户id查询菜单列表 --><selectid="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缓存根据用户ID查出来的菜单信息

Redis详解

9.1 Redis的配置类

@ConfigurationpublicclassRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactory redisConnectionFactory){RedisTemplate<String,Object> redisTemplate=newRedisTemplate<>();//String类型Key序列器
        redisTemplate.setKeySerializer(newStringRedisSerializer());//String类型Value序列器
        redisTemplate.setValueSerializer(newGenericJackson2JsonRedisSerializer());//Hash类型的key序列器
        redisTemplate.setHashKeySerializer(newStringRedisSerializer());//Hash类型的Value序列器
        redisTemplate.setHashValueSerializer(newGenericJackson2JsonRedisSerializer());

        redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}}

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

@RestControllerAdvicepublicclassGlobalException{@ExceptionHandler(SQLException.class)publicRespBeanrespBeanMysqlException(SQLException e){if(e instanceofSQLIntegrityConstraintViolationException){returnRespBean.error("该数据有关联数据,操作失败");}
        e.printStackTrace();returnRespBean.error("数据库异常,操作失败");}@ExceptionHandler(DateException.class)publicRespBeanrespBeanDateException(DateException e){
        e.printStackTrace();returnRespBean.error(e.getMessage());}@ExceptionHandler(Exception.class)publicRespBeanrespBeanException(Exception e){
        e.printStackTrace();returnRespBean.error("未知错误,请联系管理员");}}

3.基础信息设置模块

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

<1>部门管理

1.1获取所有部门

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

<selectid="getAllDepartments"resultMap="DepartmentWithChildren">
    select
    <includerefid="Base_Column_List"/>
    from t_department
    where parentId=#{parentId}
</select><!-- 通用查询映射结果 --><resultMapid="BaseResultMap"type="org.example.server.pojo.Department"><idcolumn="id"property="id"/><resultcolumn="name"property="name"/><resultcolumn="parentId"property="parentId"/><resultcolumn="depPath"property="depPath"/><resultcolumn="enabled"property="enabled"/><resultcolumn="isParent"property="isParent"/></resultMap><resultMapid="DepartmentWithChildren"type="org.example.server.pojo.Department"extends="BaseResultMap"><collectionproperty="children"ofType="org.example.server.pojo.Department"select="org.example.server.mapper.DepartmentMapper.getAllDepartments"column="id"></collection></resultMap><!-- 通用查询结果列 --><sqlid="Base_Column_List">
        id, name, parentId, depPath, enabled, isParent
    </sql>

1.2 添加部门

<!--添加部门 --><!--statementType="CALLABLE 调用存储过程--><selectid="addDep"statementType="CALLABLE">
    call addDep(#{name,mode=IN,jdbcType=VARCHAR},#{parentId,mode=IN,jdbcType=INTEGER},#{enabled,mode=IN,jdbcType=BOOLEAN},#{result,mode=OUT,jdbcType=INTEGER},#{id,mode=OUT,jdbcType=INTEGER})
</select>

1.3删除部门

<!--添加部门 --><!--statementType="CALLABLE 调用存储过程--><select id="addDep" statementType="CALLABLE">
    call addDep(#{name,mode=IN,jdbcType=VARCHAR},#{parentId,mode=IN,jdbcType=INTEGER},#{enabled,mode=IN,jdbcType=BOOLEAN},#{result,mode=OUT,jdbcType=INTEGER},#{id,mode=OUT,jdbcType=INTEGER})</select>

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

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

<1>获取全部操作员

  • Controller层@ApiOperation(value ="获取所有操作员")@GetMapping("/")publicList<Admin>getAllAdmins(String keywords){return adminService.getAllAdmins(keywords);}
  • Service层/** * 获取所有操作员 * @param keywords */@OverridepublicList<Admin>getAllAdmins(String keywords){//要传当前登录的Id,当前操作员不用查return adminMapper.getAllAdmins(AdminUtil.getCurrentAdmin().getId(),keywords);}
  • Mapper层<!--获取所有操作员 --><select id="getAllAdmins" resultMap="AdminWithRole"> SELECT a.*, r.id AS rid, r.`name` AS rname, r.nameZh AS rnameZh FROM t_admin a LEFT JOIN t_admin_role ar ON a.id = ar.adminId LEFT JOIN t_role r ON r.id = ar.rid WHERE a.id != #{id}<if test="null!=keywords and ''!=keywords"> AND a.`name` LIKECONCAT('%', #{keywords},'%')</if> ORDER BY a.id</select>涉及操作员角色的查询

<2>操作员角色的修改

Service层:

/**
 * 更新操作员角色
 * @param adminId
 * @param rids
 * @return
 */@Override@TransactionalpublicRespBeanupdateAdminRole(Integer adminId,Integer[] rids){//先将已经拥有的角色全部删除
    adminRoleMapper.delete(newQueryWrapper<AdminRole>().eq("adminId",adminId));//再将传过来的所有角色添加Integer result = adminRoleMapper.addAdminRole(adminId, rids);if(result == rids.length){returnRespBean.success("修改角色成功");}returnRespBean.error("更新角色失败");}

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

5.员工模块管理

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

  • Controller@ApiOperation(value ="查询所有的员工(分页)")@GetMapping("/")//beginDateScope入职的日期范围publicRespPageBeangetEmployee(@RequestParam(defaultValue ="1")Integer currentPage,@RequestParam(defaultValue ="10")Integer size,Employee employee,LocalDate[] beginDateScope){return employeeService.getEmployeeByPage(currentPage,size,employee,beginDateScope);}
  • Service层@OverridepublicRespPageBeangetEmployeeByPage(Integer currentPage,Integer size,Employee employee,LocalDate[] beginDateScope){Page<Employee> page=newPage<>(currentPage,size);IPage<Employee> iPage=employeeMapper.getEmployeeByPage(page,employee,beginDateScope);RespPageBean respPageBean=newRespPageBean(); respPageBean.setTotal(iPage.getTotal()); respPageBean.setData(iPage.getRecords());return respPageBean;}
  • Mapper层<resultMapid="EmployeeInfo"type="org.example.server.pojo.Employee"extends="BaseResultMap"><associationproperty="nation"javaType="org.example.server.pojo.Nation"><idcolumn="nid"property="id"/><resultcolumn="nname"property="name"/></association><associationproperty="politicsStatus"javaType="org.example.server.pojo.PoliticsStatus"><idcolumn="pid"property="id"/><resultcolumn="pname"property="name"/></association><associationproperty="department"javaType="org.example.server.pojo.Department"><idcolumn="did"property="id"/><resultcolumn="dname"property="name"/></association><associationproperty="joblevel"javaType="org.example.server.pojo.Joblevel"><idcolumn="jid"property="id"/><resultcolumn="jname"property="name"/></association><associationproperty="position"javaType="org.example.server.pojo.Position"><idcolumn="posid"property="id"/><resultcolumn="posname"property="name"/></association></resultMap><!-- 将员工的政治面貌,职称,民族,职位,部门等信息填充进去 --><!-- 获取所有员工(分页) --><selectid="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 <iftest="null!=employee.name and ''!=employee.name"> AND e.`name` LIKE CONCAT( '%', #{employee.name}, '%' ) </if><iftest="null!=employee.politicId"> AND e.politicId = #{employee.politicId} </if><iftest="null!=employee.nationId"> AND e.nationId = #{employee.nationId} </if><iftest="null!=employee.jobLevelId"> AND e.jobLevelId = #{employee.jobLevelId} </if><iftest="null!=employee.posId"> AND e.posId = #{employee.posId} </if><iftest="null!=employee.engageForm and ''!=employee.engageForm"> AND e.engageForm = #{employee.engageForm} </if><iftest="null!=employee.departmentId"> AND e.departmentId = #{employee.departmentId} </if><iftest="null!=beginDateScope and 2==beginDateScope.length"> AND e.beginDate BETWEEN #{beginDateScope[0]} AND #{beginDateScope[1]} </if> ORDER BY e.id</select>

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

2.1EasyPOI注解的使用

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

2.2 员工数据的导出

@ApiOperation(value ="导出员工数据")@GetMapping(value ="/export",produces ="application/octet-stream")publicvoidexportEmployee(HttpServletResponse response){List<Employee> list = employeeService.getEmployee(null);//参数:文件名,表名,导出的Excel的类型(03版本)ExportParams params=newExportParams("员工表","员工表",ExcelType.HSSF);Workbook workbook =ExcelExportUtil.exportExcel(params,Employee.class, list);//输入workbookServletOutputStream 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();}}}}

2.3 员工数据的导入

@ApiOperation(value ="导入员工数据")@PostMapping("/import")publicRespBeanimportEmployee(MultipartFile file){//准备导入的数据表ImportParams params=newImportParams();//去掉第一行:标题行
    params.setTitleRows(1);List<Nation> nationList = nationService.list();List<PoliticsStatus> politicsStatusList=politicsStatusService.list();List<Department> departmentList=departmentService.list();List<Joblevel> joblevelList=joblevelService.list();List<Position> positionList=positionService.list();try{//将Excel表变为ListList<Employee> list =ExcelImportUtil.importExcel(file.getInputStream(),Employee.class, params);
        list.forEach(employee ->{//获取民族IDInteger nationId = nationList.get(nationList.indexOf(newNation(employee.getNation().getName()))).getId();
            employee.setNationId(nationId);//获取政治面貌IdInteger politicsStatusId=politicsStatusList.get(politicsStatusList.indexOf(newPoliticsStatus(employee.getPoliticsStatus().getName()))).getId();
            employee.setPoliticId(politicsStatusId);//获取部门IdInteger departmentId=departmentList.get(departmentList.indexOf(newDepartment(employee.getDepartment().getName()))).getId();
            employee.setDepartmentId(departmentId);//获取职称IdInteger joblevelId=joblevelList.get(joblevelList.indexOf(newJoblevel(employee.getJoblevel().getName()))).getId();
            employee.setJobLevelId(joblevelId);//获取职位IdInteger positionId=positionList.get(positionList.indexOf(newPosition(employee.getPosition().getName()))).getId();
            employee.setPosId(positionId);});if(employeeService.saveBatch(list)){returnRespBean.success("导入成功");}}catch(Exception e){
        e.printStackTrace();}returnRespBean.error("导入失败");}

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

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

3.1 RabbitMQ消息发送的可靠性

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

在这里插入图片描述

步骤:

  • 发送消息时,将当前消息数据存入数据库,投递状态为消息投递中
  • 开启消息确认回调机制。确认成功,更新投递状态为消息投递成功
  • 开启定时任务,重新投递失败的消息。重试超过3次,更新投递状态为投递失败1. > 消息延迟投递,做二次确认,回调检查

在这里插入图片描述

步骤:

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

3.2消息功能的实现

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

//获取合同开始和结束的时间LocalDate beginContact=employee.getBeginContract();LocalDate endContact=employee.getEndContract();long days = beginContact.until(endContact,ChronoUnit.DAYS);//保留两位小数DecimalFormat decimalFormat=newDecimalFormat("##.00");
    employee.setContractTerm(Double.parseDouble(decimalFormat.format(days/365.00)));if(employeeMapper.insert(employee)==1){//获取新插入的员工对象Employee emp=employeeMapper.getEmployee(employee.getId()).get(0);//数据库记录发送的消息String msgId = UUID.randomUUID().toString();MailLog mailLog=newMailLog();
        mailLog.setMsgId(msgId);
        mailLog.setEid(employee.getId());
        mailLog.setStatus(0);//消息的状态保存在Model中
        mailLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
        mailLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);
        mailLog.setCount(MailConstants.MAX_TRY_COUNT);
        mailLog.setTryTime(LocalDateTime.now().plusMinutes(MailConstants.MAX_TRY_COUNT));
        mailLog.setCreateTime(LocalDateTime.now());
        mailLog.setUpdateTime(LocalDateTime.now());
        mailLogMapper.insert(mailLog);//发送信息//发送交换机,路由键,用户对象和消息ID
        rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,MailConstants.MAIL_ROUTING_KEY_NAME,
                emp,newCorrelationData(msgId));returnRespBean.success("添加成功");}returnRespBean.error("添加失败");}

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

@ComponentpublicclassMailReceiver{//日志privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(MailReceiver.class);@AutowiredprivateJavaMailSender javaMailSender;@AutowiredprivateMailProperties mailProperties;@AutowiredprivateTemplateEngine templateEngine;@AutowiredprivateRedisTemplate redisTemplate;@RabbitListener(queues =MailConstants.MAIL_QUEUE_NAME)//拿取Message 和 channel 可以拿到 消息序号鉴别消息是否统一个消息多收    通过消息序号+msgId两个来鉴别publicvoidhandler(Message message,Channel channel){Employee employee =(Employee) message.getPayload();MessageHeaders headers = message.getHeaders();//消息序号long tag =(long) headers.get(AmqpHeaders.DELIVERY_TAG);//拿到存取的UUIDString msgId =(String) headers.get("spring_returned_message_correlation");//这个key固定HashOperations hashOperations = redisTemplate.opsForHash();try{//从Redis中拿取,如果存在,说明消息已经发送成功了,这里直接确认返回if(hashOperations.entries("mail_log").containsKey(msgId)){
                LOGGER.error("消息已经被消费=============>{}",msgId);/**
                 * 手动确认消息
                 * tag:消息序号
                 * multiple:是否确认多条
                 */
                channel.basicAck(tag,false);return;}MimeMessage msg = javaMailSender.createMimeMessage();MimeMessageHelper helper =newMimeMessageHelper(msg);//发件人
            helper.setFrom(mailProperties.getUsername());//收件人
            helper.setTo(employee.getEmail());//主题
            helper.setSubject("入职欢迎邮件");//发送日期
            helper.setSentDate(newDate());//邮件内容Context context =newContext();//用于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模板中的信息转为StringString mail = templateEngine.process("mail", context);
            helper.setText(mail,true);//发送邮件
            javaMailSender.send(msg);
            LOGGER.info("邮件发送成功");//将消息id存入redis//mail_log是Redis  hash的key   msgId是真正的key  "OK"是Value,主要是拿到msgId,"OK"没啥用
            hashOperations.put("mail_log", msgId,"OK");//手动确认消息
            channel.basicAck(tag,false);}catch(Exception e){/**
             * 手动确认消息
             * tag:消息序号
             * multiple:是否确认多条
             * requeue:是否退回到队列
             */try{
                channel.basicNack(tag,false,true);}catch(IOException ex){
                LOGGER.error("邮件发送失败=========>{}", e.getMessage());}
            LOGGER.error("邮件发送失败=========>{}", e.getMessage());}}}

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

@ConfigurationpublicclassRabbitMQConfig{privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(RabbitMQConfig.class);@AutowiredprivateCachingConnectionFactory cachingConnectionFactory;@AutowiredprivateIMailLogService mailLogService;@BeanpublicRabbitTemplaterabbitTemplate(){RabbitTemplate rabbitTemplate =newRabbitTemplate(cachingConnectionFactory);/**
         * 消息确认回调,确认消息是否到达broker
         * data:消息的唯一标识
         * ack:确认结果
         * cause:失败原因
         */
        rabbitTemplate.setConfirmCallback((data,ack,cause)->{String msgId = data.getId();if(ack){
                LOGGER.info("{}======>消息发送成功",msgId);
                mailLogService.update(newUpdateWrapper<MailLog>().set("status",1).eq("msgId",msgId));}else{
                LOGGER.error("{}=====>消息发送失败",msgId);}});/**
         * 消息失败回调,比如router不到queue时回调
         * msg:消息的主题
         * repCode:响应码
         * repText:响应描述
         * exchange:交换机
         * routingkey:路由键
         */
        rabbitTemplate.setReturnCallback((msg,repCode,repText,exchange,routingkey)->{
            LOGGER.error("{}=====>消息发送queue时失败",msg.getBody());});return rabbitTemplate;}@BeanpublicQueuequeue(){returnnewQueue(MailConstants.MAIL_QUEUE_NAME);}@BeanpublicDirectExchangedirectExchange(){returnnewDirectExchange(MailConstants.MAIL_EXCHANGE_NAME);}@BeanpublicBindingbinding(){returnBindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);}

6.在线聊天功能的实现

这里使用WebSocket

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

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

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

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

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

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

在这里插入图片描述

WebSocket的配置

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

@Configuration@EnableWebSocketMessageBrokerpublicclassWebSocketConfigimplementsWebSocketMessageBrokerConfigurer{@Value("${jwt.tokenHead}")privateString tokenHead;@AutowiredprivateJwtTokenUtil jwtTokenUtil;@AutowiredprivateUserDetailsService userDetailsService;/**
     * 添加这个Endpoint,这样在网页可以通过websocket连接上服务
     * 也就是我们配置websocket的服务地址,并且可以指定是否使用socketJS
     * @param registry
     */@OverridepublicvoidregisterStompEndpoints(StompEndpointRegistry registry){/**
         * 1.将ws/ep路径注册为stomp的端点,用户连接了这个端点就可以进行websocket通讯,支持socketJS
         * 2.setAllowedOrigins("*"):允许跨域
         * 3.withSockJS():支持socketJS访问
         */
        registry.addEndpoint("/ws/ep").setAllowedOrigins("*").withSockJS();}/**
     * 输入通道参数配置  JWT配置
     * @param registration
     */@OverridepublicvoidconfigureClientInboundChannel(ChannelRegistration registration){
        registration.interceptors(newChannelInterceptor(){@OverridepublicMessage<?>preSend(Message<?> message,MessageChannel channel){StompHeaderAccessor accessor =MessageHeaderAccessor.getAccessor(message,StompHeaderAccessor.class);//判断是否为连接,如果是,需要获取token,并且设置用户对象if(StompCommand.CONNECT.equals(accessor.getCommand())){//拿取TokenString token = accessor.getFirstNativeHeader("Auth-Token");//参数前端已经固定if(!StringUtils.isEmpty(token)){String authToken = token.substring(tokenHead.length());String username = jwtTokenUtil.getUsernameFormToken(authToken);//token中存在用户名if(!StringUtils.isEmpty(username)){//登录UserDetails userDetails = userDetailsService.loadUserByUsername(username);//验证token是否有效,重新设置用户对象if(jwtTokenUtil.TokenIsValid(authToken,userDetails)){UsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(userDetails,null,
                                                userDetails.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                                accessor.setUser(authenticationToken);}}}}return message;}});}/**
     * 配置消息代理
     * @param registry
     */@OverridepublicvoidconfigureMessageBroker(MessageBrokerRegistry registry){//配置代理域,可以配置多个,配置代理目的地前缀为/queue,可以在配置域上向客户端推送消息
        registry.enableSimpleBroker("/queue");}}

本文转载自: https://blog.csdn.net/qq_45704528/article/details/119699269
版权归原作者 Serendipity sn 所有, 如有侵权,请联系我们删除。

“基于SpringBoot + MyBatis的前后端分离实现在线办公系统”的评论:

还没有评论