在線辦公系統
目錄
- 在線辦公系統
- 1.開發環境的搭建及專案介紹
- 2.登錄模塊及配置框架搭建
- <1>Jwt工具類及對Token的處理
- 1.1根據用戶資訊生成Token
- 1.2根據Token生成用戶名
- 1.3判斷Token是否有效
- 1.4判斷Token是否可以被重繪
- 1.5重繪Token,獲取新的Token
- <2>登錄功能的實作
- <3>退出登錄
- <4>獲取當前登錄用戶資訊
- <5>SpringSecurity的配置類SecurityConfig
- 5.1 覆寫SpringSecurity默認生成的賬號密碼,并讓他走我們自定義的登錄邏輯
- 5.2進行資源的攔截,權限設定,登錄過濾器設定
- 5.2.1登錄過濾器的配置
- 5.2.2添加未登錄結果處理器
- 5.2.3添加權限不足結果處理器
- 5.2.4添加權限控制器,根據請求的URL確定訪問該URL需要什么角色
- 5.2.5添加權限控制器,對角色資訊進行處理,是否可用訪問URL
- <6>Swagger2的配置
- <7>驗證碼功能(這里使用谷歌的驗證碼Captcha)
- 7.1驗證碼的配置類
- 7.2驗證碼的控制器
- <8>根據用戶ID查詢用戶所擁有操控權限的選單串列
- <9>使用Redis快取根據用戶ID查出來的選單資訊
- 9.1 Redis的配置類
- <10>全域例外的統一處理
- 3.基礎資訊設定模塊
- <1>部門管理
- 1.1獲取所有部門
- 1.2 添加部門
- 1.3洗掉部門
- 4.薪資模塊及薪資管理模塊
- <1>獲取全部操作員
- <2>操作員角色的修改
- 5.員工模塊管理
- <1>分頁獲取全部員工資訊
- <2>使用EasyPOI對員工資訊進行匯入和匯出
- 2.1EasyPOI注解的使用
- 2.2 員工資料的匯出
- 2.3 員工資料的匯入
- <3>使用RabbitMQ對新入職的員工發送歡迎郵件
- 3.1 RabbitMQ訊息發送的可靠性
- 3.2訊息功能的實作
- 6.在線聊天功能的實作
- 原始碼獲取
1.開發環境的搭建及專案介紹
本專案目的是實作中小型企業的在線辦公系統,云E辦在線辦公系統是一個用來管理日常的辦公事務的一個系統
使用SpringSecurity做安全認證及權限管理,Redis做快取,RabbitMq做郵件的發送,使用EasyPOI實作對員工資料的匯入和匯出,使用WebSocket做在線聊天
使用驗證碼登錄

頁面展示:

-
添加依賴
-
使用MyBatis的AutoGenerator自動生成mapper,service,Controller
2.登錄模塊及配置框架搭建
<1>Jwt工具類及對Token的處理
1.1根據用戶資訊生成Token
-
定義JWT負載中用戶名的Key以及創建時間的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運算式對應的內容, 那個
* default_value,就是前面的值為空時的默認值,注意二者的不同,#{}里面那個obj代表物件,
*/
//JWT密鑰
@Value("${jwt.secret}")
private String secret;
//JWT失效時間
@Value("${jwt.expiration}")
private Long expiration;
-
根據用戶資訊UserDetials生成Token
/**
* 根據用戶資訊生成Token
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails){
//荷載
Map<String,Object> claim=new HashMap<>();
claim.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
claim.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claim);
}
/**
* 根據負載生成JWT Token
* @param claims
* @return
*/
private String generateToken(Map<String,Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())//添加失效時間
.signWith(SignatureAlgorithm.HS512,secret)//添加密鑰以及加密方式
.compact();
}
/**
* 生成Token失效時間 當前時間+配置的失效時間
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis()+expiration*1000);
}
1.2根據Token生成用戶名
/**
* 根據Token生成用戶名
* @param token
* @return
*/
public String getUsernameFormToken(String token){
String username;
//根據Token去拿荷載
try {
Claims claim=getClaimFromToken(token);
username=claim.getSubject();//獲取用戶名
} catch (Exception e) {
e.printStackTrace();
username=null;
}
return username;
}
/**
* 從Token中獲取荷載
* @param token
* @return
*/
private Claims getClaimFromToken(String token) {
Claims claims=null;
try {
claims=Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
1.3判斷Token是否有效
/**
* 判斷Token是否有效
* Token是否過期
* Token中的username和UserDetails中的username是否一致
* @param token
* @param userDetails
* @return
*/
public boolean TokenIsValid(String token,UserDetails userDetails){
String username = getUsernameFormToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判斷Token是否過期
* @param token
* @return
*/
private boolean isTokenExpired(String token) {
//獲取Token的失效時間
Date expireDate=getExpiredDateFromToken(token);
//在當前時間之前,則失效
return expireDate.before(new Date());
}
/**
* 獲取Token的失效時間
* @param token
* @return
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimFromToken(token);
return claims.getExpiration();
}
1.4判斷Token是否可以被重繪
/**
* 判斷token是否可用被重繪
* 如果已經過期了,則可用被重繪,未過期,則不可用被重繪
* @param token
* @return
*/
public boolean canRefresh(String token){
return !isTokenExpired(token);
}
1.5重繪Token,獲取新的Token
/**
* 重繪Token
* @param token
* @return
*/
public String refreshToken(String token){
Claims claims=getClaimFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
<2>登錄功能的實作
-
Controller層
@ApiOperation(value = "登錄之后回傳token") @PostMapping("/login") //AdminLoginParam 自定義登錄時傳入的物件,包含賬號,密碼,驗證碼 public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){ return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request); } -
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在資料庫查出這個物件 //在SecurityConfig組態檔中,重寫了loadUserByUsername方法,回傳了userDetailsService Bean物件,使用我們自己的登錄邏輯 UserDetails userDetails = userDetailsService.loadUserByUsername(username); //如果userDetails為慷訓userDetails中的密碼和傳入的密碼不相同 if (userDetails == null||!passwordEncoder.matches(password,userDetails.getPassword())){ return RespBean.error("用戶名或密碼不正確"); } //判斷賬號是否可用 if(!userDetails.isEnabled()){ return RespBean.error("該賬號已經被禁用,請聯系管理員"); } //更新登錄用戶物件,放入security全域中,密碼不放 UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //生成token String token = jwtTokenUtil.generateToken(userDetails); Map<String,String> tokenMap=new HashMap<>(); tokenMap.put("token",token); tokenMap.put("tokenHead",tokenHead);//tokenHead,從組態檔yml中拿到的token的請求頭 == Authorization return RespBean.success("登陸成功",tokenMap);//將Token回傳 }
<3>退出登錄
退出登錄功能由前端實作,我們只需要回傳一個成功資訊即可
@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); //不能回傳前端用戶密碼,設定為空 admin.setPassword(null); //將用戶角色回傳 admin.setRoles(adminService.getRoles(admin.getId())); return admin; }
<5>SpringSecurity的配置類SecurityConfig
5.1 覆寫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方法實作自定義登錄邏輯
5.2進行資源的攔截,權限設定,登錄過濾器設定
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用Jwt不需要csrf
http.csrf().disable()
//基于token,不需要Session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//授權認證
.authorizeRequests()
.antMatchers("/doc.html").permitAll()
//除了上面,所有的請求都要認證
.anyRequest()
.authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
//動態權限配置
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(customUrlDecisionManager);
o.setSecurityMetadataSource(customFilter);
return o;
}
})
.and()
//禁用快取
.headers()
.cacheControl();
//添加jwt登錄授權過濾器 判斷是否登錄
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定義未授權和未登錄結果回傳
http.exceptionHandling()
//權限不足
.accessDeniedHandler(restfulAccessDeniedHandler)
//未登錄
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
//將登錄過濾器注入
@Bean
public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){
return new JwtAuthencationTokenFilter();
}
//需要放行的資源
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
//首頁
"/index.html",
//網頁圖示
"favicon.ico",
//Swagger2
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
//放行影像驗證碼
"/captcha",
//WebSocket
"/ws/**"
);
}
5.2.1登錄過濾器的配置
public class JwtAuthencationTokenFilter extends OncePerRequestFilter {
//Jwt存盤頭
@Value("${jwt.tokenHeader}")
private String tokenHeader;
//Jwt頭部資訊
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//token存盤在Jwt的請求頭中
//通過key:tokenHeader拿到value:token
//這里我們定義的token后期以:Bearer開頭,空格分割,加上真正的jwt
//通過tokenHeader(Authorization)拿到以Bearer開頭 空格分割 加上真正的jwt的字串
String authHeader = httpServletRequest.getHeader(tokenHeader);
//判斷這個token的請求頭是否為空且是以配置資訊中要求的tokenHead開頭
if(authHeader != null && authHeader.startsWith(tokenHead)){
//截取真正的jwt
String authToken=authHeader.substring(tokenHead.length());
String username=jwtTokenUtil.getUsernameFormToken(authToken);
//token存在用戶名但是未登錄
if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
//登錄
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//驗證token是否有效,重新設定用戶物件
if(jwtTokenUtil.TokenIsValid(authToken,userDetails)){
//把物件放到Security的全域中
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
//將請求中的Session等資訊放入Details,再放入Security全域中
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
//放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
5.2.2添加未登錄結果處理器
當未登錄或者Token失效時訪問未放行的介面時,自定義回傳的結果
@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();
}
}
5.2.3添加權限不足結果處理器
當訪問介面沒有權限時,自定義回傳結果
@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("權限不足,請聯系管理員");
bean.setCode(401);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
5.2.4添加權限控制器,根據請求的URL確定訪問該URL需要什么角色
@Component
public class CustomFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
private IMenuService menuService;
AntPathMatcher antPathMatcher=new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//獲取請求的URL
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> menus = menuService.getMenuWithRole();
//將URL所需要的角色放入Menu中
for (Menu menu:menus) {
//判斷請求Url與選單角色擁有的url是否匹配
if(antPathMatcher.match(menu.getUrl(),requestUrl)){
// 該Url所需要的角色
String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
//如果匹配上放入配置中,需要的角色
return SecurityConfig.createList(str);
}
}
//沒匹配的url默認登錄即可訪問
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
5.2.5添加權限控制器,對角色資訊進行處理,是否可用訪問URL
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Autowired
private CustomFilter customFilter;
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute: collection) {
// 當前url所需要的角色
List<ConfigAttribute> list= (List<ConfigAttribute>) customFilter.getAttributes(o);
String[] needRoles=new String[list.size()];
for (int i = 0; i <list.size() ; i++) {
needRoles[i]=list.get(i).getAttribute();
}
//判斷角色是否登錄即可訪問的角色,此角色在CustomFilter中設定
for (String needRole:needRoles) {
if ("ROLE_LOGIN".equals((needRole))) {
//判斷是否已經登錄
if(authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("尚未登錄,請登錄");
}else {
return;
}
}
}
//判斷用戶角色是否為url所需要的角色
//得到用戶擁有的角色 這里在Admin類中已經將用戶的角色放入了
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (String needRole:needRoles) {
for (GrantedAuthority authority: authorities) {
if(authority.getAuthority().equals(needRole)){
return;
}
}
}
throw new AccessDeniedException("權限不足,請聯系管理員");
}
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return false;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
<6>Swagger2的配置
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
//基礎設定
.apiInfo(apiInfo())
//掃描哪個包
.select()
.apis(RequestHandlerSelectors.basePackage("org.example.server.controller"))
//任何路徑都可以
.paths(PathSelectors.any())
.build()
.securityContexts(securityContexts())
.securitySchemes(securitySchemes());
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("云E辦介面檔案")
.description("云E辦介面檔案")
.contact(new Contact("朱云飛", "http:localhost:8081/doc.html","2690534598@qq.com"))
.version("1.0")
.build();
}
private List<ApiKey> securitySchemes(){
//設定請求頭資訊
List<ApiKey> result=new ArrayList<>();
ApiKey apiKey=new ApiKey("Authorization", "Authorization","Header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts(){
//設定需要登錄認證的路徑
List<SecurityContext> result=new ArrayList<>();
result.add(getContextByPath("/hello/.*"));
return result;
}
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())//添加全域認證
.forPaths(PathSelectors.regex(pathRegex)) //帶有pathRegex欄位的介面訪問不帶添加的Authorization全域變數
.build();
}
//添加Swagger全域的Authorization 全域認證 固定的代碼
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result=new ArrayList<>();
//設定范圍為全域
AuthorizationScope authorizationScope=new AuthorizationScope("global","accessEeverything");
AuthorizationScope[]authorizationScopes=new AuthorizationScope[1];
authorizationScopes[0]=authorizationScope;
result.add((new SecurityReference("Authorization",authorizationScopes)));//這里的Authorization和上文ApiKey第二個引數一致
return result;
}
}
注意:
ApiKey apiKey=new ApiKey("Authorization", "Authorization","Header");

<7>驗證碼功能(這里使用谷歌的驗證碼Captcha)
7.1驗證碼的配置類
@Component
public class CaptchaConfig {
@Bean
public DefaultKaptcha defaultKaptcha(){
//驗證碼生成器
DefaultKaptcha defaultKaptcha=new DefaultKaptcha();
//配置
Properties properties = new Properties();
//是否有邊框
properties.setProperty("kaptcha.border", "yes");
//設定邊框顏色
properties.setProperty("kaptcha.border.color", "105,179,90");
//邊框粗細度,默認為1
// properties.setProperty("kaptcha.border.thickness","1");
//驗證碼
properties.setProperty("kaptcha.session.key","code");
//驗證碼文本字符顏色 默認為黑色
properties.setProperty("kaptcha.textproducer.font.color", "blue");
//設定字體樣式
properties.setProperty("kaptcha.textproducer.font.names", "宋體,楷體,微軟雅黑");
//字體大小,默認40
properties.setProperty("kaptcha.textproducer.font.size", "30");
//驗證碼文本字符內容范圍 默認為abced2345678gfynmnpwx
// properties.setProperty("kaptcha.textproducer.char.string", "");
//字符長度,默認為5
properties.setProperty("kaptcha.textproducer.char.length", "4");
//字符間距 默認為2
properties.setProperty("kaptcha.textproducer.char.space", "4");
//驗證碼圖片寬度 默認為200
properties.setProperty("kaptcha.image.width", "100");
//驗證碼圖片高度 默認為40
properties.setProperty("kaptcha.image.height", "40");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
7.2驗證碼的控制器
@RestController
public class CaptchaController {
@Autowired
private DefaultKaptcha defaultKaptcha;
@ApiOperation(value = "驗證碼")
@GetMapping(value = "/captcha",produces = "image/jpeg")
public void captcha(HttpServletRequest request, HttpServletResponse response){
// 定義response輸出型別為image/jpeg型別
response.setDateHeader("Expires", 0);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
// return a jpeg
response.setContentType("image/jpeg");
//-------------------生成驗證碼 begin --------------------------
//獲取驗證碼文本內容
String text=defaultKaptcha.createText();
System.out.println("驗證碼內容"+text);
//將驗證碼文本內容放入Session
request.getSession().setAttribute("captcha",text);
//根據文本驗證碼內容創建圖形驗證碼
BufferedImage image = defaultKaptcha.createImage(text);
ServletOutputStream outputStream=null;
try {
outputStream = response.getOutputStream();
//輸出流輸出圖片,格式為jpg
ImageIO.write(image, "jpg",outputStream);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(outputStream !=null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//-------------------生成驗證碼 end --------------------------
}
}
<8>根據用戶ID查詢用戶所擁有操控權限的選單串列

-
Controller層
@ApiOperation(value = "通過用戶ID查詢選單串列") @GetMapping("/menu") public List<Menu> getMenuByAdminId(){ return menuService.getMenuByAdminId(); } -
Service層
@Override public List<Menu> getMenuByAdminId() { //從Security全域背景關系中獲取當前登錄用戶Admin Admin admin= AdminUtil.getCurrentAdmin(); Integer adminId=admin.getId(); ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue(); //從Redis獲取選單資料 List<Menu> menus = (List<Menu>) valueOperations.get("menu_" + adminId); //如果為空,從資料庫中獲取 if(CollectionUtils.isEmpty(menus)){ menus=menuMapper.getMenuByAdminId(adminId); //查詢之后放入Redis valueOperations.set("menu_"+adminId,menus); } return menus; } -
Mapper層
<!-- 根據用戶id查詢選單串列 --> <select id="getMenuByAdminId" resultMap="Menus"> SELECT DISTINCT m1.*, m2.id AS id2, m2.url AS url2, m2.path AS path2, m2.component AS component2, m2.`name` AS name2, m2.iconCls AS iconCls2, m2.keepAlive AS keepAlive2, m2.requireAuth AS requireAuth2, m2.parentId AS parentId2, m2.enabled AS enabled2 FROM t_menu m1, t_menu m2, t_admin_role ar, t_menu_role mr WHERE m1.id = m2.parentId AND m2.id = mr.mid AND mr.rid = ar.rid AND ar.adminId = #{id} AND m2.enabled = TRUE ORDER BY m2.id </select>
<9>使用Redis快取根據用戶ID查出來的選單資訊
Redis詳解
9.1 Redis的配置類
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
//String型別Key序列器
redisTemplate.setKeySerializer(new StringRedisSerializer());
//String型別Value序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//Hash型別的key序列器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//Hash型別的Value序列器
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
<10>全域例外的統一處理
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler(SQLException.class)
public RespBean respBeanMysqlException(SQLException e){
if(e instanceof SQLIntegrityConstraintViolationException){
return RespBean.error("該資料有關聯資料,操作失敗");
}
e.printStackTrace();
return RespBean.error("資料庫例外,操作失敗");
}
@ExceptionHandler(DateException.class)
public RespBean respBeanDateException(DateException e){
e.printStackTrace();
return RespBean.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public RespBean respBeanException(Exception e){
e.printStackTrace();
return RespBean.error("未知錯誤,請聯系管理員");
}
}
3.基礎資訊設定模塊
職位,職稱,權限組管理僅涉及單表的增刪查改,這里不多寫
<1>部門管理
1.1獲取所有部門
Mapper層:涉及父子類,遞回查找
<select id="getAllDepartments" resultMap="DepartmentWithChildren">
select
<include refid="Base_Column_List"/>
from t_department
where parentId=#{parentId}
</select>
<!-- 通用查詢映射結果 -->
<resultMap id="BaseResultMap" type="org.example.server.pojo.Department">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="parentId" property="parentId" />
<result column="depPath" property="depPath" />
<result column="enabled" property="enabled" />
<result column="isParent" property="isParent" />
</resultMap>
<resultMap id="DepartmentWithChildren" type="org.example.server.pojo.Department" extends="BaseResultMap">
<collection property="children" ofType="org.example.server.pojo.Department" select="org.example.server.mapper.DepartmentMapper.getAllDepartments"
column="id">
</collection>
</resultMap>
<!-- 通用查詢結果列 -->
<sql id="Base_Column_List">
id, name, parentId, depPath, enabled, isParent
</sql>
1.2 添加部門
<!--添加部門 -->
<!--statementType="CALLABLE 呼叫存盤程序-->
<select id="addDep" statementType="CALLABLE">
call addDep(#{name,mode=IN,jdbcType=VARCHAR},#{parentId,mode=IN,jdbcType=INTEGER},#{enabled,mode=IN,jdbcType=BOOLEAN},#{result,mode=OUT,jdbcType=INTEGER},#{id,mode=OUT,jdbcType=INTEGER})
</select>
1.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("/") public List<Admin> getAllAdmins(String keywords){ return adminService.getAllAdmins(keywords); } -
Service層
/** * 獲取所有操作員 * @param keywords */ @Override public List<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` LIKE CONCAT( '%', #{keywords}, '%' ) </if> ORDER BY a.id </select>涉及操作員角色的查詢
<2>操作員角色的修改
Service層:
/**
* 更新操作員角色
* @param adminId
* @param rids
* @return
*/
@Override
@Transactional
public RespBean updateAdminRole(Integer adminId, Integer[] rids) {
//先將已經擁有的角色全部洗掉
adminRoleMapper.delete(new QueryWrapper<AdminRole>().eq("adminId",adminId));
//再將傳過來的所有角色添加
Integer result = adminRoleMapper.addAdminRole(adminId, rids);
if(result == rids.length){
return RespBean.success("修改角色成功");
}
return RespBean.error("更新角色失敗");
}
思想:先將操作員所有的角色都洗掉,再將前端闖入的角色全部添加
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<Employee> page=new Page<>(currentPage,size); IPage<Employee> iPage=employeeMapper.getEmployeeByPage(page,employee,beginDateScope); RespPageBean respPageBean=new RespPageBean(); respPageBean.setTotal(iPage.getTotal()); respPageBean.setData(iPage.getRecords()); return respPageBean; } -
Mapper層
<resultMap id="EmployeeInfo" type="org.example.server.pojo.Employee" extends="BaseResultMap"> <association property="nation" javaType="org.example.server.pojo.Nation"> <id column="nid" property="id" /> <result column="nname" property="name" /> </association> <association property="politicsStatus" javaType="org.example.server.pojo.PoliticsStatus"> <id column="pid" property="id" /> <result column="pname" property="name" /> </association> <association property="department" javaType="org.example.server.pojo.Department"> <id column="did" property="id" /> <result column="dname" property="name" /> </association> <association property="joblevel" javaType="org.example.server.pojo.Joblevel"> <id column="jid" property="id" /> <result column="jname" property="name" /> </association> <association property="position" javaType="org.example.server.pojo.Position"> <id column="posid" property="id" /> <result column="posname" property="name" /> </association> </resultMap> <!-- 將員工的政治面貌,職稱,民族,職位,部門等資訊填充進去 --> <!-- 獲取所有員工(分頁) --> <select id="getEmployeeByPage" resultMap="EmployeeInfo"> SELECT e.*, n.id AS nid, n.`name` AS nname, p.id AS pid, p.`name` AS pname, d.id AS did, d.`name` AS dname, j.id AS jid, j.`name` AS jname, pos.id AS posid, pos.`name` AS posname FROM t_employee e, t_nation n, t_politics_status p, t_department d, t_joblevel j, t_position pos WHERE e.nationId = n.id AND e.politicId = p.id AND e.departmentId = d.id AND e.jobLevelId = j.id AND e.posId = pos.id <if test="null!=employee.name and ''!=employee.name"> AND e.`name` LIKE CONCAT( '%', #{employee.name}, '%' ) </if> <if test="null!=employee.politicId"> AND e.politicId = #{employee.politicId} </if> <if test="null!=employee.nationId"> AND e.nationId = #{employee.nationId} </if> <if test="null!=employee.jobLevelId"> AND e.jobLevelId = #{employee.jobLevelId} </if> <if test="null!=employee.posId"> AND e.posId = #{employee.posId} </if> <if test="null!=employee.engageForm and ''!=employee.engageForm"> AND e.engageForm = #{employee.engageForm} </if> <if test="null!=employee.departmentId"> AND e.departmentId = #{employee.departmentId} </if> <if test="null!=beginDateScope and 2==beginDateScope.length"> AND e.beginDate BETWEEN #{beginDateScope[0]} AND #{beginDateScope[1]} </if> ORDER BY e.id </select>
<2>使用EasyPOI對員工資訊進行匯入和匯出
2.1EasyPOI注解的使用

用于員工資料匯入:Excel表中的部門,職稱等欄位在資料庫員工表中找不到欄位,資料庫中是以id外鍵欄位存盤


2.2 員工資料的匯出
@ApiOperation(value = "匯出員工資料")
@GetMapping(value = "/export",produces = "application/octet-stream")
public void exportEmployee(HttpServletResponse response){
List<Employee> list = employeeService.getEmployee(null);
//引數:檔案名,表名,匯出的Excel的型別(03版本)
ExportParams params=new ExportParams("員工表","員工表", ExcelType.HSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, Employee.class, list);
//輸入workbook
ServletOutputStream out=null;
try{
//流形式
response.setHeader("content-type","application/octet-stream");
//防止中文亂碼
response.setHeader("content-disposition","attachment;filename="+ URLEncoder.encode("員工表.xls","UTF-8"));
out = response.getOutputStream();
workbook.write(out);
}catch (IOException e){
e.printStackTrace();
}finally {
if(out != null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2.3 員工資料的匯入
@ApiOperation(value = "匯入員工資料")
@PostMapping("/import")
public RespBean importEmployee(MultipartFile file){
//準備匯入的資料表
ImportParams params=new ImportParams();
//去掉第一行:標題行
params.setTitleRows(1);
List<Nation> nationList = nationService.list();
List<PoliticsStatus> politicsStatusList=politicsStatusService.list();
List<Department> departmentList=departmentService.list();
List<Joblevel> joblevelList=joblevelService.list();
List<Position> positionList=positionService.list();
try {
//將Excel表變為List
List<Employee> list = ExcelImportUtil.importExcel(file.getInputStream(), Employee.class, params);
list.forEach(employee -> {
//獲取民族ID
Integer nationId = nationList.get(nationList.indexOf(new Nation(employee.getNation().getName()))).getId();
employee.setNationId(nationId);
//獲取政治面貌Id
Integer politicsStatusId=politicsStatusList.get(politicsStatusList.indexOf(new PoliticsStatus(employee.getPoliticsStatus().getName()))).getId();
employee.setPoliticId(politicsStatusId);
//獲取部門Id
Integer departmentId=departmentList.get(departmentList.indexOf(new Department(employee.getDepartment().getName()))).getId();
employee.setDepartmentId(departmentId);
//獲取職稱Id
Integer joblevelId=joblevelList.get(joblevelList.indexOf(new Joblevel(employee.getJoblevel().getName()))).getId();
employee.setJobLevelId(joblevelId);
//獲取職位Id
Integer positionId=positionList.get(positionList.indexOf(new Position(employee.getPosition().getName()))).getId();
employee.setPosId(positionId);
});
if(employeeService.saveBatch(list)){
return RespBean.success("匯入成功");
}
} catch (Exception e) {
e.printStackTrace();
}
return RespBean.error("匯入失敗");
}
<3>使用RabbitMQ對新入職的員工發送歡迎郵件
這里使用SMTP:需要先去郵箱開通SMTP服務
3.1 RabbitMQ訊息發送的可靠性
-
訊息落庫,對訊息狀態進行標記

步驟:
-
發送訊息時,將當前訊息資料存入資料庫,投遞狀態為訊息投遞中
-
開啟訊息確認回呼機制,確認成功,更新投遞狀態為訊息投遞成功
-
開啟定時任務,重新投遞失敗的訊息,重試超過3次,更新投遞狀態為投遞失敗
-
訊息延遲投遞,做二次確認,回呼檢查
-

步驟:
- 發送訊息時,將當前訊息存入資料庫,訊息狀態為訊息投遞
- 過一段時間進行第二次的訊息發送
- 開啟訊息回呼機制,當第一次發送的訊息被成功消費時,消費端的確認會被MQ Broker監聽,成功則將訊息佇列中的狀態變為投遞成功
- 如果訊息投遞沒有成功,則過一段時間第二次發送的訊息也會被MQ Broker監聽到,會根據這條訊息的ID去訊息資料庫查找,如果發現訊息資料庫中的狀態為投遞中而不是投遞成功,則會通知訊息放松端重新進行步驟一
3.2訊息功能的實作
在進行新員工插入成功后,對新員工發出郵件,并將發送的郵件保存到資料庫中
//獲取合同開始和結束的時間
LocalDate beginContact=employee.getBeginContract();
LocalDate endContact=employee.getEndContract();
long days = beginContact.until(endContact, ChronoUnit.DAYS);
//保留兩位小數
DecimalFormat decimalFormat=new DecimalFormat("##.00");
employee.setContractTerm(Double.parseDouble(decimalFormat.format(days/365.00)));
if(employeeMapper.insert(employee) == 1){
//獲取新插入的員工物件
Employee emp=employeeMapper.getEmployee(employee.getId()).get(0);
//資料庫記錄發送的訊息
String msgId = UUID.randomUUID().toString();
MailLog mailLog=new MailLog();
mailLog.setMsgId(msgId);
mailLog.setEid(employee.getId());
mailLog.setStatus(0);
//訊息的狀態保存在Model中
mailLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
mailLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);
mailLog.setCount(MailConstants.MAX_TRY_COUNT);
mailLog.setTryTime(LocalDateTime.now().plusMinutes(MailConstants.MAX_TRY_COUNT));
mailLog.setCreateTime(LocalDateTime.now());
mailLog.setUpdateTime(LocalDateTime.now());
mailLogMapper.insert(mailLog);
//發送資訊
//發送交換機,路由鍵,用戶物件和訊息ID
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,
MailConstants.MAIL_ROUTING_KEY_NAME,
emp,
new CorrelationData(msgId));
return RespBean.success("添加成功");
}
return RespBean.error("添加失敗");
}
消費端的處理,這里我們使用上述第一種方式,—>訊息落庫,對訊息狀態進行標記. 為保證消費者不重復消費同一訊息,采取 訊息序號+我們傳入的訊息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 可以拿到 訊息序號鑒別訊息是否統一個訊息多收 通過訊息序號+msgId兩個來鑒別
public void handler(Message message, Channel channel) {
Employee employee = (Employee) message.getPayload();
MessageHeaders headers = message.getHeaders();
//訊息序號
long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
//拿到存取的UUID
String msgId = (String) headers.get("spring_returned_message_correlation");//這個key固定
HashOperations hashOperations = redisTemplate.opsForHash();
try {
//從Redis中拿取,如果存在,說明訊息已經發送成功了,這里直接確認回傳
if (hashOperations.entries("mail_log").containsKey(msgId)){
LOGGER.error("訊息已經被消費=============>{}",msgId);
/**
* 手動確認訊息
* tag:訊息序號
* multiple:是否確認多條
*/
channel.basicAck(tag,false);
return;
}
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg);
//發件人
helper.setFrom(mailProperties.getUsername());
//收件人
helper.setTo(employee.getEmail());
//主題
helper.setSubject("入職歡迎郵件");
//發送日期
helper.setSentDate(new Date());
//郵件內容
Context context = new Context();
//用于theymeleaf獲取
context.setVariable("name", employee.getName());
context.setVariable("posName", employee.getPosition().getName());
context.setVariable("joblevelName", employee.getJoblevel().getName());
context.setVariable("departmentName", employee.getDepartment().getName());
//將準備好的theymeleaf模板中的資訊轉為String
String mail = templateEngine.process("mail", context);
helper.setText(mail, true);
//發送郵件
javaMailSender.send(msg);
LOGGER.info("郵件發送成功");
//將訊息id存入redis
//mail_log是Redis hash的key msgId是真正的key "OK"是Value,主要是拿到msgId,"OK"沒啥用
hashOperations.put("mail_log", msgId, "OK");
//手動確認訊息
channel.basicAck(tag, false);
} catch (Exception e) {
/**
* 手動確認訊息
* tag:訊息序號
* multiple:是否確認多條
* requeue:是否退回到佇列
*/
try {
channel.basicNack(tag,false,true);
} catch (IOException ex) {
LOGGER.error("郵件發送失敗=========>{}", e.getMessage());
}
LOGGER.error("郵件發送失敗=========>{}", e.getMessage());
}
}
}
訊息的配置類,確認應答等
@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);
/**
* 訊息確認回呼,確認訊息是否到達broker
* data:訊息的唯一標識
* ack:確認結果
* cause:失敗原因
*/
rabbitTemplate.setConfirmCallback((data,ack,cause)->{
String msgId = data.getId();
if(ack){
LOGGER.info("{}======>訊息發送成功",msgId);
mailLogService.update(new UpdateWrapper<MailLog>().set("status",1 ).eq("msgId",msgId));
}else {
LOGGER.error("{}=====>訊息發送失敗",msgId);
}
});
/**
* 訊息失敗回呼,比如router不到queue時回呼
* msg:訊息的主題
* repCode:回應碼
* repText:回應描述
* exchange:交換機
* routingkey:路由鍵
*/
rabbitTemplate.setReturnCallback((msg,repCode,repText,exchange,routingkey)->{
LOGGER.error("{}=====>訊息發送queue時失敗",msg.getBody());
});
return rabbitTemplate;
}
@Bean
public Queue queue(){
return new Queue(MailConstants.MAIL_QUEUE_NAME);
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange(MailConstants.MAIL_EXCHANGE_NAME);
}
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);
}
6.在線聊天功能的實作
這里使用WebSocket
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議,
WebSocket 使得客戶端和服務器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料,
在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連
接,并進行雙向資料傳輸,
它的最大特點就是,服務器可以主動向客戶端推送資訊,客戶端也可以主動向服務器發送資訊,是真正
的雙向平等對話,屬于服務器推送技術的一種,

WebSocket的配置
這里主要是前端實作,后端只是增加一些配置
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
/**
* 添加這個Endpoint,這樣在網頁可以通過websocket連接上服務
* 也就是我們配置websocket的服務地址,并且可以指定是否使用socketJS
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* 1.將ws/ep路徑注冊為stomp的端點,用戶連接了這個端點就可以進行websocket通訊,支持socketJS
* 2.setAllowedOrigins("*"):允許跨域
* 3.withSockJS():支持socketJS訪問
*/
registry.addEndpoint("/ws/ep").setAllowedOrigins("*").withSockJS();
}
/**
* 輸入通道引數配置 JWT配置
* @param registration
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
//判斷是否為連接,如果是,需要獲取token,并且設定用戶物件
if (StompCommand.CONNECT.equals(accessor.getCommand())){
//拿取Token
String token = accessor.getFirstNativeHeader("Auth-Token");//引數前端已經固定
if (!StringUtils.isEmpty(token)){
String authToken = token.substring(tokenHead.length());
String username = jwtTokenUtil.getUsernameFormToken(authToken);
//token中存在用戶名
if (!StringUtils.isEmpty(username)){
//登錄
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//驗證token是否有效,重新設定用戶物件
if (jwtTokenUtil.TokenIsValid(authToken,userDetails)){
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
accessor.setUser(authenticationToken);
}
}
}
}
return message;
}
});
}
/**
* 配置訊息代理
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//配置代理域,可以配置多個,配置代理目的地前綴為/queue,可以在配置域上向客戶端推送訊息
registry.enableSimpleBroker("/queue");
}
}
原始碼獲取
專案原始碼獲取:在線辦公系統專案原始碼
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/293926.html
標籤:其他
