# Meta-U **Repository Path**: xzxwbb/Meta-U ## Basic Information - **Project Name**: Meta-U - **Description**: 社区兼后台管理 技术选型:SpringBoot+SpringSecurity+JWT+MyBatis-Plus+PageHelper+Redis+ElasticSearch+FastDFS+Task - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2022-09-21 - **Last Updated**: 2022-10-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: 开源 ## README [TOC] --- ## 目录结构❤️ ### 1.meta-u-web 入口 > 程序启动的入口,主要都是controller - common : 通用 - system : 后台管理系统 - community : 社区 --- ### 2.meta-u-service 业务逻辑 --- ### 3.meta-u-common 公共常量,工具类,解析器,序列化等 --- ### 4.meta-u-model pojo/dto/vo --- ### 5.meta-u-framework 架构的核心配置,拦截器,处理器,配置类等 #### 5.1.后台管理系统 角色认证,权限控制 过滤器链: 1. 判断用户是否登录 : 解析前期头中的`token`拿到`用户名`,根据用户名查询`用户实体` ```java /** * JWT登录授权过滤器 * * @author xzx on 2022/4/23 */ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private RedisCache redisCache; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { /** * 区分不同领域的请求: 比如现在community的所有接口只要登录就可以 */ String requestUrl = request.getRequestURI(); if (requestUrl.startsWith("/community")) { filterChain.doFilter(request, response); } else { /** * system的处理方式 */ String authHeader = request.getHeader(tokenHeader); //存在token if (null != authHeader && authHeader.startsWith(tokenHead)) { String authToken = authHeader.substring(tokenHead.length()); String username = jwtTokenUtil.getUserNameFromToken(authToken); //token存在用户名但未登录 if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) { //登录 UserDetails userDetails = userDetailsService.loadUserByUsername(username); //验证token是否有效,重新设置用户对象 if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(request, response); } } } ``` 2. 根据`请求URL获取访问它所需的角色列表`(满足一个即可) ```java /** * 自定义过滤器 * 权限控制 * 根据请求url分析出请求所需角色 * 由于用到了service层,故放置在这里 * * @author xzx * @since 2022/4/28 */ @Component public class CustomFilter implements FilterInvocationSecurityMetadataSource { @Autowired private IMenuService menuService; AntPathMatcher antPathMatcher = new AntPathMatcher(); /** * 根据请求url匹配到有该权限的角色中去 * * @param object * @return 该请求需要的角色 * @throws IllegalArgumentException */ @Override public Collection getAttributes(Object object) throws IllegalArgumentException { //获取请求的url String requestUrl = ((FilterInvocation) object).getRequestUrl(); //获取所有的menus和它们对象的role List menus = menuService.getMenusWithRole(); for (Menu menu : menus) { //判断请求url与菜单角色是否匹配 if (antPathMatcher.match(menu.getUrl(), requestUrl)) { //将路径匹配的角色通过map流提取出来 String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::new); return SecurityConfig.createList(str); } } //没匹配的url默认登录即可访问 return SecurityConfig.createList("ROLE_LOGIN"); } @Override public Collection getAllConfigAttributes() { return null; } @Override public boolean supports(Class clazz) { return true; } } ``` 3. `获取改请求用户授权的所有角色,只要存在一个与2的角色列表对应`,证明用户拥有权限访问 ```java /** * 自定义 URL 决策管理器 * 权限控制 * 判断用户角色 * * @author xzx * @since 2022/4/28 */ @Component public class CustomUrlDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { //configAttributes: 是一个拦截器的根据url获取的拥有权限的roles for (ConfigAttribute configAttribute : configAttributes) { //当前url所需角色 String needRole = configAttribute.getAttribute(); //判断角色是否登录即可访问的角色,此角色在CustomFilter中设置 if ("ROLE_LOGIN".equals(needRole)) { //判断是否登录 if (authentication instanceof AnonymousAuthenticationToken) { throw new AccessDeniedException("尚未登录,请登录!"); } else { return; } } //判断用户角色是否为url所需角色 Collection authorities = authentication.getAuthorities(); //authorities为该用户所拥有的角色 for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needRole)) { return; } } } throw new AccessDeniedException("权限不足,请联系管理员!"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class clazz) { return true; } } ``` ![image-20220925231321862](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20220925231321862.png) --- 配置类`SecurityConfig` ```java /** * Security配置类 * 由于用到了service层,故放置在这里 * 且要配置复杂的业务逻辑 * * @author xzx on 2022/4/23 */ @Configuration @SuppressWarnings("all") public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IAdminService adminService; @Autowired private RestAccessDeniedHandler accessDeniedHandler; @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint; @Autowired private CustomFilter customFilter; @Autowired private CustomUrlDecisionManager customUrlDecisionManager; /** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/**/login", "/**/register", "/**/captcha", "/**/logout", "/common/**", "/community/**") // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**") .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**"); } @Override protected void configure(HttpSecurity http) throws Exception { //使用JWT,不需要csrf http.csrf() .disable() //基于token,不需要session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() //所有请求都要求认证 .anyRequest().authenticated() //动态权限配置 .withObjectPostProcessor(new ObjectPostProcessor() { @Override public O postProcess(O object) { object.setAccessDecisionManager(customUrlDecisionManager); object.setSecurityMetadataSource(customFilter); return object; } }) .and() //禁用缓存 .headers() .cacheControl(); //添加JWT filter 登录授权过滤器 http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); //添加自定义未授权和未登录结果返回 http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); //添加CORS filter http.addFilterBefore(corsFilter, JwtAuthenticationFilter.class); } @Override @Bean public UserDetailsService userDetailsService() { return username -> { Admin admin = adminService.getAdminByUserName(username); if (ObjUtil.isNotNull(admin)) { return admin; } throw new UsernameNotFoundException("用户名或密码不正确"); }; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } } ``` --- #### 5.2.社区 `由JWT + Redis + UserHolder 实现` ```java @Configuration public class MVCConfig implements WebMvcConfigurer { @Autowired private RedisCache redisCache; @Autowired private JwtTokenUtil jwtTokenUtil; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/community/**") .excludePathPatterns("/community/login") .order(1); registry.addInterceptor(new RefreshTokenInterceptor(redisCache, jwtTokenUtil)) .addPathPatterns("/community/**") .excludePathPatterns("/community/login") .order(0); } } ``` ```java /** * 登录拦截器:需要new()出来,有config传递bean类 * * @author xzx * @since 2022/8/2 */ public class LoginInterceptor implements HandlerInterceptor { /** * 校验是否登录 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = UserHolder.getUser(); if(user == null) { errResp(response); return false; } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } private void errResp(HttpServletResponse response) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter out = response.getWriter(); out.write(new ObjectMapper().writeValueAsString(ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN))); out.flush(); out.close(); } } ``` ```java /** * refresh token * * @author xzx * @since 2022/8/2 */ public class RefreshTokenInterceptor implements HandlerInterceptor { public static final String tokenHeader = "Authorization"; public static final String tokenHead = "Bearer"; private RedisCache redisCache; private JwtTokenUtil jwtTokenUtil; public RefreshTokenInterceptor(RedisCache redisCache, JwtTokenUtil jwtTokenUtil) { this.redisCache = redisCache; this.jwtTokenUtil = jwtTokenUtil; } /** * 校验token验证身份 * * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authHeader = request.getHeader(tokenHeader); if (StrUtil.isBlank(authHeader) || !authHeader.startsWith(tokenHead)) { return true; } String authToken = authHeader.substring(tokenHead.length() + 1); String username = jwtTokenUtil.getUserNameFromToken(authToken); String tokenKey = LOGIN_USER_KEY + username; User user = (User) redisCache.getCacheObject(tokenKey); /** * 用户未登录或者已经token过期 */ if (ObjUtil.isNull(user)) { return true; } UserHolder.saveUser(user); /** * token存在,延期,利用redis来对token续期 */ redisCache.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } } ``` ```java public class UserHolder { private static final ThreadLocal tl = new ThreadLocal<>(); public static void saveUser(User user) { tl.set(user); } public static User getUser() { return tl.get(); } public static void removeUser() { tl.remove(); } public static Integer getUserId() { return tl.get().getId(); } } ``` --- #### 5.3.配置跨域 ```java /** * 配置跨域 * * @author xzx * @date 2022/9/26 */ @Configuration public class CorsConfig { /** * 跨域配置 */ @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 设置访问源地址 config.addAllowedOriginPattern("*"); // 设置访问源请求头 config.addAllowedHeader("*"); // 设置访问源请求方法 config.addAllowedMethod("*"); // 允许任何域名使用 // config.addAllowedOrigin("*"); // 有效期 1800秒 config.setMaxAge(1800L); // 添加映射路径,拦截一切请求 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // 返回新的CorsFilter return new CorsFilter(source); } } ``` --- ### 6.meta-u-generator 代码生成器 > 利用MyBatis-Plus的代码生成器,快速生成controller-service(impl)-mapper(xml)-pojo的目录结构和代码 --- ## 后台管理👍 ### 1.RBAC权限模型 ``` RBAC权限模型( ``` `Role-Based Access Control`)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。 ![image-20211222110249727](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20211222110249727.png) --- ### 2.树形菜单 `核心代码` ```java /** * 未知根节点的ID,通过自探寻来判断第一层(最外层)的节点 * * @param menus :获取的平级数组 * @return */ public List getChildPermsWithRootId(List menus) { List resultMenus = new ArrayList(); //获取的平级数组 //找到最上层的parentId for (int i = 0; i < menus.size(); i++) { Menu menu = menus.get(i); for (int j = 0; j < menus.size(); j++) { //判断得出该节点存在列表中存在父节点,跳出循环 if (menu.getParentId() == menus.get(j).getId()) { break; } //该节点没有父节点,是为第一层的节点,以它本身为父节点构建树形结构 if (j == menus.size() - 1) { //获取子菜单 recursionFn(menus, menu); resultMenus.add(menu); } } } return resultMenus; } /** * 根据父节点的ID获取所有子节点 * * @param list 平级的菜单表 * @param parentId 传入的父节点ID 根节点默认0 * @return String */ private List getChildPerms(List list, int parentId) { List returnList = new ArrayList(); //遍历每一个菜单 for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { Menu t = (Menu) iterator.next(); // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 if (t.getParentId() == parentId) { //获取子菜单 recursionFn(list, t); returnList.add(t);//添加一级菜单 } } return returnList; } /** * 递归列表 * * @param list 平级结构 * @param t 当前节点 */ private void recursionFn(List list, Menu t) { // 得到子节点列表 List childList = getChildList(list, t); t.setChildren(childList); //遍历子节点,构建子节点的树状结构 for (Menu tChild : childList) { //判断该节点是否还有子节点 if (hasChild(list, tChild)) { recursionFn(list, tChild); } } } /** * 得到子节点列表 * 1.遍历菜单 * 2.判断pId = mId */ private List getChildList(List list, Menu t) { List tlist = new ArrayList(); Iterator it = list.iterator(); while (it.hasNext()) { Menu n = (Menu) it.next(); if (n.getParentId() == t.getId()) { tlist.add(n); } } return tlist; } /** * 判断是否有子节点 */ private boolean hasChild(List list, Menu t) { return getChildList(list, t).size() > 0; } ``` --- ## 社区功能😕 ### 帖子 发帖人,标签,标题,封面,内容缩略,观看,评论,点赞数量 #### 1.设计原型 ![image-20221001215719118](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20221001215719118.png) ![image-20221001215942223](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20221001215942223.png) 内容:富文本 点赞、收藏、评论、转发数据 发布人、时间、内容(文字,排版,图片,表情等) 主要是内容这一方面比较难做, 排版:富文本编辑器,(支持md格式),博客系统 简单实现: 每添加一张图片就上传,拿到url,最好前端传的数据是html,后端直接存储标签结构化数据 ![image-20221001212829739](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20221001212829739.png) ![image-20221001213340854](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20221001213340854.png) --- #### 2.数据库设计 ```sql CREATE TABLE `mu_blog` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', `author_id` int NOT NULL COMMENT '作者id', `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容', `type` int NOT NULL COMMENT 'url对应的数据类型;1:图片 2:视频', `show_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'http:localhost/' COMMENT '展示的url', `created_by` int NOT NULL COMMENT '创建人', `created_time` datetime NOT NULL COMMENT '创建时间', `updated_by` int NOT NULL COMMENT '更新人', `updated_time` datetime NOT NULL COMMENT '更新时间', `like_cnt` int NOT NULL DEFAULT '0' COMMENT '点赞数量', `unlike_cnt` int NOT NULL DEFAULT '0' COMMENT '不喜欢数量', `comment_cnt` int NOT NULL DEFAULT '0' COMMENT '评论数量', `collect_cnt` int NOT NULL DEFAULT '0' COMMENT '收藏数量', `relay_cnt` int NOT NULL DEFAULT '0' COMMENT '转发数量', `author_name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL, `author_avatar_url` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL, `tag_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, `tag_id` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='博客'; ``` --- ### 评论 也可以是文字+图片,还是得简单的富文本 ![image-20221001215951227](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20221001215951227.png) 结构设计类似B站、米游社的评论区 ![image-20221001215926895](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20221001215926895.png) ![image-20220929135132748](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20220929135132748.png) ![image-20220929135116867](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20220929135116867.png) ![image-20220929135157127](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20220929135157127.png) ![image-20220929135230533](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20220929135230533.png) --- #### 1.一级评论 > ``` > 不分页,支持排序(热度or时间) > ``` 关联哪一篇帖子,评论者的用户信息(用户名、头像等),内容,点赞数,回复数,发表时间 1. 数据库设计 ```sql CREATE TABLE `mu_parent_comment` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', `created_by` int NOT NULL COMMENT '创建人', `created_time` datetime NOT NULL COMMENT '创建时间', `updated_by` int NOT NULL COMMENT '更新人', `updated_time` datetime NOT NULL COMMENT '更新时间', `blog_id` int NOT NULL COMMENT '帖子id', `user_id` int NOT NULL COMMENT '用户id', `username` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', `user_avatar_url` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户头像url', `like_cnt` int NOT NULL DEFAULT '0' COMMENT '点赞数', `unlike_cnt` int NOT NULL DEFAULT '0' COMMENT '不喜欢数', `content` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='一级评论区'; ``` --- #### 2.二级评论 > 支持分页,排序(默认时间) 评论者信息,内容,点赞数量,踩数量 关联一级评论,评论者的信息,普通的回复(or@某个二级评论的回复), 该字段为null表示是普通的回复,否则是对改字段的二级评论的回复 `reply_id为0表示普通回复,否则表示@用户编号reply_id的用户` 1. 数据库设计 ```sql CREATE TABLE `mu_child_comment` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', `created_by` int NOT NULL COMMENT '创建人', `created_time` datetime NOT NULL COMMENT '创建时间', `updated_by` int NOT NULL COMMENT '更新人', `updated_time` datetime NOT NULL COMMENT '更新时间', `parent_id` int NOT NULL COMMENT '一级评论区id', `user_id` int NOT NULL COMMENT '用户id', `username` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', `user_avatar_url` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户头像url', `reply_id` int NOT NULL DEFAULT '0' COMMENT '回复对象的id', `reply_username` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '回复对象的用户名', `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '内容', `like_cnt` int NOT NULL DEFAULT '0' COMMENT '点赞数', `unlike_cnt` int NOT NULL DEFAULT '0' COMMENT '踩数', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='二级评论区'; ``` --- ## ElasticSearch搜索引擎👀️ ### 1.引入依赖 ```xml org.elasticsearch.client elasticsearch-rest-high-level-client ``` ### 2.自定义搜索DTO ```java /** * es搜索引擎参数 * * @author xzx * @date 2022/10/4 */ @Data @Accessors(chain = true) public class SearchReqDto { /** * 当前页码 */ private Integer pageNum = 1; /** * 页数 */ private Integer pageSize = 10; /** * 索引库名称 */ private String indices; /** * 关键词 */ private String keyword; /** * 排序字段 */ private String orderByColumn = "createdTime"; /** * asc or desc */ private String isAsc = "asc"; /** * 高亮字段 */ private String[] highlightFields; } ``` ### 3.封装工具类、实现`分页、高亮、排序` ```java /** * elasticsearch配置类 * * @author xzx * @date 2022/10/2 */ @Configuration public class ESConfig { @Value("${elasticsearch.url}") private String elasticsearchUrl; @Bean(name = "RestHighLevelClient") public RestHighLevelClient restHighLevelClient() { return new RestHighLevelClient(RestClient.builder( HttpHost.create(elasticsearchUrl) ) ); } } ``` ```java /** * elasticsearch 封装类 * * @author xzx * @date 2022/10/4 */ @Component public class EsClient { @Autowired private RestHighLevelClient restHighLevelClient; /** * 搜索并完成分页、高亮、排序 * * @param req * @param beanClass * @return responseResult * @throws IOException */ public ResponseResult findObject(SearchReqDto req, Class beanClass) throws IOException { List resultList = this.findList(req, beanClass); return ResponseResult.okResult(PageUtil.buildResultMap(resultList)); } /** * 搜索并完成分页、高亮、排序 * * @param req * @param beanClass * @return List * @throws IOException */ public List findList(SearchReqDto req, Class beanClass) throws IOException { if (ObjUtil.isNull(req) || StringUtil.isBlank(req.getIndices())) { return Collections.emptyList(); } if (StringUtil.isBlank(req.getOrderByColumn())) { req.setOrderByColumn("createdTime"); } List resultList = new ArrayList<>(); SearchHits hits = queryBuilder(req); T bean = null; //遍历查询结果并接收 for (SearchHit searchHit : hits) { /** * 关键词为空或者没有高亮字段,不需要再处理 */ if (StringUtil.isBlank(req.getKeyword()) || ArrayUtil.isEmpty(req.getHighlightFields())) { bean = JSONUtil.toBean(searchHit.getSourceAsString(), beanClass); } else { bean = replaceAttr(searchHit, req.getHighlightFields(), beanClass); } if (ObjUtil.isNotNull(bean)) { resultList.add(bean); } } return resultList; } /** * 构造查询条件,获取原生返回数据 * * @param req 查询条件 * @return SearchHit * @throws IOException */ public SearchHits queryBuilder(SearchReqDto req) throws IOException { SearchSourceBuilder builder = new SearchSourceBuilder(); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); HighlightBuilder highlightBuilder = new HighlightBuilder(); int pageNum = req.getPageNum() > 0 ? req.getPageNum() : 1; int pageSize = req.getPageSize() > 0 ? req.getPageSize() : 10; //设置分页 builder.from((pageNum - 1) * pageSize); builder.size(pageSize); //设置高亮 highlightBuilder.field("*"); //所有的字段都高亮 highlightBuilder.requireFieldMatch(false);//如果要多个字段高亮,这项要为false highlightBuilder.preTags("").postTags("") .fragmentSize(800000)//下面这两项,如果你要高亮如文字内容等有很多字的字段,必须配置,不然会导致高亮不全,文章内容缺失等; 最大高亮分片数 .numOfFragments(0);//从第一个分片获取高亮片段 builder.highlighter(highlightBuilder); //模糊查询 if (StringUtil.isBlank(req.getKeyword())) { boolQueryBuilder.should(QueryBuilders.matchAllQuery()); } else { boolQueryBuilder.should(QueryBuilders.matchQuery("all", req.getKeyword())); } //对查询结果进行排序 SortOrder order = SortOrder.ASC; if (SortOrder.DESC.name().equalsIgnoreCase(req.getIsAsc())) { order = SortOrder.DESC; } builder.query(boolQueryBuilder).sort(req.getOrderByColumn(), order); SearchRequest searchRequest = new SearchRequest(req.getIndices()); searchRequest.source(builder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); return searchResponse.getHits(); } /** * 将高亮字段替换 * * @param searchHit 查询结果 * @param attrs 字段名 * @return T */ private T replaceAttr(SearchHit searchHit, String[] attrs, Class beanClass) { T bean = null; Map highlightFields = searchHit.getHighlightFields(); for (String field : attrs) { HighlightField hField = highlightFields.get(field); if (hField != null) { //替换高亮字段 Text[] fragments = hField.fragments(); StringBuilder text = new StringBuilder(); for (Text textGet : fragments) { text.append(textGet); } //对象的值只能读一次,否则会被覆盖 if (ObjUtil.isNull(bean)) { //转换数据类型 bean = JSONUtil.toBean(searchHit.getSourceAsString(), beanClass); } //设置对象的属性值 ReflectUtil.invokeSetter(bean, field, text.toString()); } } return bean; } } ``` > 反射工具类**ReflectUtil**是我自定义的,原理是反射,可以引入hutool的工具包,同样也可以实现该结果 ![image-20221009160754939](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20221009160754939.png) --- ## PageHelper分页功能🚀️ ```yml # PageHelper分页插件 pagehelper: helperDialect: mysql supportMethodsArguments: true params: count=countSql ``` ```java /** * 请求分页参数和条件 * * @author xzx * @date 2022/9/21 */ @Data @Accessors(chain = true) public class PageReqDto { /** * 当前记录起始索引 */ private Integer pageNum; /** * 每页显示记录数 */ private Integer pageSize; /** * 排序列 */ private String orderByColumn; /** * 排序的方向desc或者asc */ private String isAsc = "asc"; /** * 分页参数合理化 */ private Boolean reasonable = true; } ``` ```java /** * web层通用数据处理 基础控制器 * * @author xzx * @date 2022/9/27 */ @Slf4j public class BaseController { public void startPage() { PageUtils.startPage(); } } ``` ```java /** * @author xzx * @date 2022/9/27 */ public class PageUtils extends PageHelper { /** * 当前记录起始索引 */ public static final String PAGE_NUM = "pageNum"; /** * 每页显示记录数 */ public static final String PAGE_SIZE = "pageSize"; /** * 排序列 */ public static final String ORDER_BY_COLUMN = "orderByColumn"; /** * 排序的方向 "desc" 或者 "asc". */ public static final String IS_ASC = "isAsc"; /** * 分页参数合理化 */ public static final String REASONABLE = "reasonable"; /** * 构造分页返回格式 * @param resultList * @return */ public static Map buildResultMap(List resultList) { Map resultMap = new HashMap<>(); resultMap.put("total", resultList.size()); resultMap.put("record", resultList); return resultMap; } /** * 设置请求分页数据 */ public static void startPage() { //1.封装分页的对象(根据请求参数设置页码,每页条数,排序字段,升序或降序) PageRequestDto dto = new PageRequestDto(); //根据键获取请求参数,设置默认的pageNum, pageSize dto.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1)); dto.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10)); dto.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN)); dto.setIsAsc(ServletUtils.getParameter(IS_ASC)); dto.setReasonable(ServletUtils.getParameterToBool(REASONABLE)); Integer pageSize = dto.getPageSize(); Integer pageNum = dto.getPageNum(); String orderBy = SqlUtil.escapeOrderBySql(dto.getOrderBy()); // reasonable: true时,pageNum<1时,会查询第一页。pageNum>最大的页码数时,会查询最后一页。 Boolean reasonable = dto.getReasonable(); //2.PageHelper开启分页 PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); } /** * 清理分页的线程变量 */ public static void clearPage() { PageHelper.clearPage(); } } ``` --- ## Redis缓存😄 ### 0.配置 ```yml # Redis配置 spring: redis: # 超时时间 timeout: 10000ms # 服务器地址 host: 119.91.201.156 # 服务器端口 port: 6379 # 数据库 database: 0 # 密码 password: 123321 # lettuce: # pool: # # 最大连接数,默认8 # max-active: 1024 # # 最大连接阻塞等待时间,默认-1 # max-wait: 10000ms # # 最大空闲连接 # max-idle: 200 # # 最小空闲连接 # min-idle: 5 jedis: pool: # 连接池中的最小空闲连接 min-idle: 2 # 连接池中的最大空闲连接 max-idle: 8 # 连接池的最大数据库连接数 max-active: 16 # #连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms ``` --- ### 1.工具类RedisCache ```java /** * Redis使用FastJson序列化 * * @author ruoyi */ public class FastJson2JsonRedisSerializer implements RedisSerializer { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class clazz; public FastJson2JsonRedisSerializer(Class clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType); } } ``` ```java /** * Redis缓存工具类 * redisTemplate 需要配置自定义序列化 * @author ruoyi **/ @SuppressWarnings(value = { "unchecked", "rawtypes" }) @Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public T getCacheObject(final String key) { ValueOperations operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public long setCacheList(final String key, final List dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public List getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public BoundSetOperations setCacheSet(final String key, final Set dataSet) { BoundSetOperations setOperation = redisTemplate.boundSetOps(key); Iterator it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public Set getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public void setCacheMap(final String key, final Map dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public Map getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key Redis键 * @param hKey Hash键 * @return Hash中的对象 */ public T getCacheMapValue(final String key, final String hKey) { HashOperations opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 删除Hash中的数据 * * @param key * @param hKey */ public void delCacheMapValue(final String key, final String hKey) { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.delete(key, hKey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public List getMultiCacheMapValue(final String key, final Collection hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection keys(final String pattern) { return redisTemplate.keys(pattern); } } ``` --- ### 2.工具类 StringRedisCache ```java /** * StringRedisTemplate缓存工具类 * StringRedisTemplate * 对象装化为json,返回json转化为对象 * * @author xzx * @since 2022/8/5 */ @Slf4j @Component public class StringRedisCache { private final StringRedisTemplate stringRedisTemplate; private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); public StringRedisCache(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * get jsonStr from redis and turn it into obj * * @param key * @param beanClass * @param 泛型 * @return */ public T get(String key, Class beanClass) { String json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isBlank(json)) { return null; } return JSONUtil.toBean(json, beanClass); } /** * 获取ZSet中其中某位成员的score * * @param key * @param member * @return */ public Double getScoreFromZSet(String key, String member) { return stringRedisTemplate.opsForZSet().score(key, member); } /** * 往ZSet中插入一位成员 * * @param key * @param member * @param score */ public void addMemberToZSet(String key, String member, Long score) { stringRedisTemplate.opsForZSet().add(key, member, score); } /** * 从ZSet中移除一名成员 * * @param key * @param member */ public void removeMemberFromZSet(String key, String member) { stringRedisTemplate.opsForZSet().remove(key, member); } /** * get jsonStr from redis and turn it into obj * * @param key * @param elementType * @param 泛型 * @return */ public List getList(String key, Class elementType) { String json = stringRedisTemplate.opsForValue().get(key); return JSONUtil.toList(json, elementType); } /** * objToJsonStr and save it in redis * * @param key * @param value * @param time * @param timeUnit */ public void set(String key, Object value, Long time, TimeUnit timeUnit) { stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit); } public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit) { //将time * timeUnit 转化为秒 RedisData redisData = new RedisData().setData(value).setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time))); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } /** * 解决缓存穿透,通过为不存在的id在redis构建键值对来隔绝大量的无效请求,减轻数据库服务器的压力 * * @param keyPreFix * @param id * @param beanClass * @param dbFallback * @param time * @param timeUnit * @param 泛型 * @param 实体类主键id * @return */ public T queryWithPassThrough( String keyPreFix, ID id, Class beanClass, Function dbFallback, Long time, TimeUnit timeUnit) { String key = keyPreFix + id; String json = stringRedisTemplate.opsForValue().get(key); // != null && !"".equals(shopJson) if (StrUtil.isNotBlank(json)) { return JSONUtil.toBean(json, beanClass); } if (json != null) { return null; } T r = dbFallback.apply(id); if (r == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } this.set(key, r, time, timeUnit); return r; } /** * 逻辑过期解决缓存击穿(针对热点key)问题 * * @param beanClass * @param keyPrefix * @param id * @param expireTime * @param expireTimeUnit * @param lockKeyPrefix * @param lockTime * @param lockTimeUnit * @param 泛型 * @param 实体类主键id * @return */ public T queryWithLogicalExpire(Class beanClass, Function dbFallback, String keyPrefix, ID id, Long expireTime, TimeUnit expireTimeUnit, String lockKeyPrefix, Long lockTime, TimeUnit lockTimeUnit) { String key = keyPrefix + id; //1.从Redis查询缓存 String json = stringRedisTemplate.opsForValue().get(key); //未命中或者为""直接结束 if (StrUtil.isBlank(json)) { return null; } //解析Json RedisData redisData = JSONUtil.toBean(json, RedisData.class); T r = JSONUtil.toBean((JSONObject) redisData.getData(), beanClass); //2.判断是否过期 if (redisData.getExpireTime().isAfter(LocalDateTime.now())) { return r; } String lockKey = lockKeyPrefix + id; boolean isLock = this.tryLock(lockKey, lockTime, lockTimeUnit); if (isLock) { CACHE_REBUILD_EXECUTOR.submit(() -> { try { T r1 = dbFallback.apply(id); this.setWithLogicalExpire(key, r1, expireTime, expireTimeUnit); } catch (Exception e) { throw new RuntimeException(e); } finally { this.unLock(lockKey); } }); } return r; } /** * 获取互斥锁 * * @param key * @return */ public boolean tryLock(String key, Long time, TimeUnit timeUnit) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", time, timeUnit); return BooleanUtil.isTrue(flag); } /** * 释放互斥锁 * * @param key */ public void unLock(String key) { stringRedisTemplate.delete(key); } } ``` --- ## FastDFS文件管理🎉️ ```yml spring: #文件上传 servlet: multipart: max-file-size: 10MB max-request-size: 10MB ``` ```java import java.io.IOException; /** * FastDFS工具类 * * @author xzx * @date 2022/9/14 */ @Slf4j public class FastDFSUtils { /** * 初始化客户端 * ClientGlobal.init 读取配置文件,并初始化对应的属性 */ static { try { String filePath = new ClassPathResource("fdfs_client_conf").getFile().getAbsolutePath(); ClientGlobal.init(filePath); } catch (Exception e) { log.error("初始化FastDFS失败:{}", e.getMessage()); } } /** * 上传文件 * * @param file * @return */ public static String[] upload(MultipartFile file) { String name = file.getOriginalFilename(); log.info("文件名:{}", name); StorageClient storageClient = null; String[] uploadResults = null; try { //获取storage客户端 storageClient = getStorageClient(); //上传 String fileExtName = name.substring(name.lastIndexOf(".") + 1); uploadResults = storageClient.upload_file(file.getBytes(), fileExtName, null); } catch (Exception e) { log.error("上传文件失败:{}", e.getMessage()); } if (ObjectUtils.isEmpty(uploadResults)) { log.error("上传失败:{}", storageClient.getErrorCode()); } return uploadResults; } /** * 获取文件信息 * * @param groupName * @param remoteFileName * @return */ public static FileInfo getFileInfo(String groupName, String remoteFileName) { StorageClient storageClient = null; try { storageClient = getStorageClient(); FileInfo fileInfo = storageClient.get_file_info(groupName, remoteFileName); return fileInfo; } catch (Exception e) { log.error("文件信息获取失败:{}", e.getMessage()); } return null; } /** * 下载文件 * * @param groupName * @param remoteFileName * @return */ public static InputStream downFile(String groupName, String remoteFileName) { StorageClient storageClient = null; try { storageClient = getStorageClient(); byte[] fileByte = storageClient.download_file(groupName, remoteFileName); InputStream inputStream = new ByteArrayInputStream(fileByte); return inputStream; } catch (Exception e) { log.error("文件下载失败:{}", e.getMessage()); } return null; } /** * 删除文件 * * @param groupName * @param remoteFileName */ public static void deleteFile(String groupName, String remoteFileName) { StorageClient storageClient = null; try { storageClient = getStorageClient(); storageClient.delete_file(groupName, remoteFileName); } catch (Exception e) { log.error("文件删除失败:{}", e.getMessage()); } } /** * 生成StorageClient客户端 * * @return * @throws IOException */ private static StorageClient getStorageClient() throws IOException { TrackerServer trackerServer = getTrackerServer(); StorageClient storageClient = new StorageClient(trackerServer, null); return storageClient; } /** * 生成tracker服务器 * * @return * @throws IOException */ private static TrackerServer getTrackerServer() throws IOException { TrackerClient trackerClient = new TrackerClient(); TrackerServer trackerServer = trackerClient.getTrackerServer(); return trackerServer; } /** * 获取文件路径 * * @return */ public static String getTrackerUrl() { TrackerClient trackerClient = new TrackerClient(); TrackerServer trackerServer = null; StorageServer storeStorage = null; try { trackerServer = trackerClient.getTrackerServer(); storeStorage = trackerClient.getStoreStorage(trackerServer); } catch (Exception e) { log.error("获取文件路径失败:{}", e.getMessage()); } return "http://" + storeStorage.getInetSocketAddress().getHostString() + ":8888/"; } } ``` --- ## Jackson配置👍 ```yml spring: # jackson配置 jackson: # json和对象的命名转换 property-naming-strategy: SNAKE_CASE date-format: yyyy-MM-dd HH:mm:ss locale: zh time-zone: GMT+8 default-property-inclusion: NON_NULL deserialization: #允许对象忽略json中不存在的属性 该字段为null,不传输 fail_on_unknown_properties: false parser: #允许出现特殊字符和转义符 allow_unquoted_control_chars: true #允许出现单引号 allow_single_quotes: true ``` --- ## Task定时任务👎 在启动类上添加注解`@EnableScheduling` ```java @SpringBootApplication @EnableScheduling ``` ```yml spring: # 定时任务配置 task: scheduling: pool: size: 2 # 任务调度线程池大小 默认 1 thread-name-prefix: scheduling- # 调度线程名称前缀 默认 scheduling- shutdown: await-termination: false # 线程池关闭时等待所有任务完成 await-termination-period: 10s # 调度线程关闭前最大等待时间,确保最后一定关闭 ``` 实例 ```java /** * @author xzx * @date 2022/10/3 */ @Component @Slf4j public class UpdateTask { @Autowired private IAuthorService authorService; @Autowired private IBlogService blogService; @Autowired private RestHighLevelClient restHighLevelClient; /** * 每隔2分钟计算每个作者的总喜欢数量、总收藏数量、更新数据库 */ @Scheduled(cron = "0 0/2 * * * ?") public void updateAuthorData() { log.warn(Thread.currentThread().getName() + " :updateAuthorData task running..."); //所有作者 List authors = authorService.list(); for (Author author : authors) { //该作者发布的博客 List blogs = blogService.lambdaQuery().select(Blog::getLikeCnt, Blog::getCollectCnt).eq(Blog::getAuthorId, author.getId()).list(); /** * stream流收集总喜欢数量、总收藏数量 */ if (CollectionUtil.isEmpty(blogs)) { continue; } Integer likeSum = blogs.stream().mapToInt(Blog::getLikeCnt).sum(); Integer collectSum = blogs.stream().mapToInt(Blog::getCollectCnt).sum(); author.setLikeCnt(likeSum).setCollectCnt(collectSum); } authorService.updateBatchById(authors); } /** * 每隔2分钟将blog同步到elasticsearch * * @throws Exception */ @Scheduled(cron = "0 0/2 * * * ?") public void updateBlogDocument() throws Exception { log.warn(Thread.currentThread().getName() + " :updateBlogDocument task running..."); BulkRequest bulkRequest = new BulkRequest(); List blogs = blogService.selectList(null); for (Blog blog : blogs) { bulkRequest.add( new UpdateRequest("blog", blog.getId().toString()) .doc(JSONUtil.toJsonStr(blog), XContentType.JSON) ); } restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT); } } ``` --- ## Log日志管理😕 ```java # 设置日志级别,root表示根节点,即整体应用日志级别 logging: # 日志级别 level: root: info # 格式 # pattern: # console: "%d %clr(%p) --- [%16t] %clr(%-40.40c){cyan} : %m %n" file: name: server.log logback: rollingpolicy: max-file-size: 1MB file-name-pattern: server.%d{yyyy-MM-dd}.%i.log ``` --- ## EasyPoi实现Excel导入导出👀️ ### 1.导入依赖 ```xml cn.afterturn easypoi-spring-boot-starter 4.4.0 ``` ### 2.在实体类的字段添加注解 ```java @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("mu_tag") @ApiModel(value = "Tag对象", description = "标签") public class Tag implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "主键id") @TableId(value = "id", type = IdType.AUTO) private Integer id; @ApiModelProperty(value = "名称") @Excel(name = "名称") private String name; @ApiModelProperty(value = "创建人") @TableField(fill = FieldFill.INSERT) @Excel(name = "创建人") private String createdBy; @ApiModelProperty(value = "创建时间") @TableField(fill = FieldFill.INSERT) @Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") private Date createdTime; @ApiModelProperty(value = "更新人") @TableField(fill = FieldFill.INSERT_UPDATE) @Excel(name = "更新人") private String updatedBy; @ApiModelProperty(value = "更新时间") @TableField(fill = FieldFill.INSERT_UPDATE) @Excel(name = "更新时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") private Date updatedTime; } ``` ### 3.封装工具类 ```java /** * Excel导入导出工具类 * * @author xzx * @date 2022/10/8 */ public class ExcelUtil { /** * 导出Excel * * @param title 标题 * @param sheetName sheetName * @param beanClass class * @param list 集合 * @param response response * @param 泛型 */ static public void export(String title, String sheetName, Class beanClass, List list, HttpServletResponse response) { ExportParams params = new ExportParams(title, sheetName, ExcelType.HSSF); Workbook workbook = ExcelExportUtil.exportExcel(params, beanClass, list); ServletOutputStream out = null; try { //流形式 response.setHeader("content-type", "application/octet-stream"); //防止中文乱码 response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(title + ".xls", "UTF-8")); out = response.getOutputStream(); workbook.write(out); } catch (IOException e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 从Excel中导入数据 * * @param file 文件 * @param beanClass class * @param service 业务层 * @param 泛型 * @param list 集合 * @return responseResult */ static public ResponseResult importExcel(MultipartFile file, Class beanClass, Object service, List list) { try { String methodName = "saveBatch"; Object success = ReflectUtil.invokeMethodByName(service, methodName, new List[]{list}); if (BooleanUtil.isTrue(Convert.toBool(success))) { return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS); } } catch (Exception e) { e.printStackTrace(); } return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR); } /** * 从Excel中导入数据 * * @param file 文件 * @param beanClass class * @param service 业务层 * @param 泛型 * @return responseResult */ static public ResponseResult importExcel(MultipartFile file, Class beanClass, Object service) { try { List list = getListFromExcel(file, beanClass); String methodName = "saveBatch"; Object success = ReflectUtil.invokeMethodByName(service, methodName, new List[]{list}); if (BooleanUtil.isTrue(Convert.toBool(success))) { return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS); } } catch (Exception e) { e.printStackTrace(); } return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR); } /** * 获取Excel的实体类集合 * * @param file 文件 * @param beanClass class * @param 泛型 * @return list * @throws Exception */ static public List getListFromExcel(MultipartFile file, Class beanClass) throws Exception { ImportParams params = new ImportParams(); params.setTitleRows(1); return ExcelImportUtil.importExcel(file.getInputStream(), beanClass, params); } } ``` ---