From fe3bff264244307e4867062400406f250ee6a0ef Mon Sep 17 00:00:00 2001 From: qingfeng1121 Date: Thu, 25 Dec 2025 13:25:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96JWT=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E5=99=A8=E5=B9=B6=E5=A2=9E=E5=BC=BA=E8=B7=A8?= =?UTF-8?q?=E5=9F=9F=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 重构安全配置和认证控制器逻辑 fix: 修正消息服务中的空值检查逻辑 feat(article): 为分类属性添加文章计数功能 chore: 删除不再使用的初始化类和字符编码配置 style: 清理代码中的无用导入和空白行 perf(cors): 添加开发环境跨域配置和响应头 docs: 更新JWT配置注释和默认密钥 test: 移除Spring Data Web分页属性导入 ci: 添加spring-devtools配置支持JJWT库 --- .../config/CharacterEncodingConfig.java | 48 ---------- .../qf/myafterprojecy/config/CorsConfig.java | 6 +- .../config/JwtAuthenticationFilter.java | 96 ++++++++----------- .../myafterprojecy/config/SecurityConfig.java | 46 +++------ .../controller/AuthController.java | 35 +++++-- .../qf/myafterprojecy/init/UserDataInit.java | 65 ------------- .../com/qf/myafterprojecy/pojo/Nonsense.java | 1 + .../pojo/dto/CategoryAttributeDto.java | 11 ++- .../pojo/dto/CategoryTreeDto.java | 9 +- .../repository/ArticleRepository.java | 9 +- .../repository/MessageRepository.java | 1 - .../service/IArticleService.java | 2 - .../service/impl/CategoryService.java | 18 +++- .../service/impl/MessageService.java | 8 +- .../service/impl/NonsenseService.java | 1 - .../META-INF/spring-devtools.properties | 5 + src/main/resources/application-dev.properties | 3 +- .../resources/application-prod.properties | 2 +- 18 files changed, 128 insertions(+), 238 deletions(-) delete mode 100644 src/main/java/com/qf/myafterprojecy/config/CharacterEncodingConfig.java delete mode 100644 src/main/java/com/qf/myafterprojecy/init/UserDataInit.java create mode 100644 src/main/resources/META-INF/spring-devtools.properties diff --git a/src/main/java/com/qf/myafterprojecy/config/CharacterEncodingConfig.java b/src/main/java/com/qf/myafterprojecy/config/CharacterEncodingConfig.java deleted file mode 100644 index e6784b3..0000000 --- a/src/main/java/com/qf/myafterprojecy/config/CharacterEncodingConfig.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.qf.myafterprojecy.config; - -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.web.filter.CharacterEncodingFilter; - -import javax.servlet.Filter; -import java.nio.charset.StandardCharsets; - -/** - * 字符编码配置类 - * 确保所有HTTP请求和响应都使用UTF-8编码,解决中文乱码问题 - */ -@Configuration -public class CharacterEncodingConfig { - - /** - * 创建字符编码过滤器 - * 优先级设置为最高,确保在所有其他过滤器之前执行 - */ - @Bean - public FilterRegistrationBean characterEncodingFilter() { - // 创建字符编码过滤器 - CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter(); - - // 设置请求编码为UTF-8 - encodingFilter.setEncoding(StandardCharsets.UTF_8.name()); - - // 强制请求使用UTF-8编码 - encodingFilter.setForceRequestEncoding(true); - - // 强制响应使用UTF-8编码 - encodingFilter.setForceResponseEncoding(true); - - // 创建过滤器注册Bean - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(encodingFilter); - - // 设置过滤器顺序为最高优先级 - registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); - - // 为所有请求路径注册过滤器 - registrationBean.addUrlPatterns("/*"); - - return registrationBean; - } -} diff --git a/src/main/java/com/qf/myafterprojecy/config/CorsConfig.java b/src/main/java/com/qf/myafterprojecy/config/CorsConfig.java index 47d3bd2..7eb3ecd 100644 --- a/src/main/java/com/qf/myafterprojecy/config/CorsConfig.java +++ b/src/main/java/com/qf/myafterprojecy/config/CorsConfig.java @@ -26,10 +26,9 @@ public class CorsConfig { @Value("${cors.allow-credentials}") private Boolean allowCredentials; - @Value("${cors.max-age:3600}") + @Value("${cors.max-age}") private Long maxAge; - /** * 创建CORS过滤器,配置跨域请求的规则 * 从配置文件中读取CORS配置,实现配置的统一管理 @@ -64,7 +63,8 @@ public class CorsConfig { config.addExposedHeader("Accept"); config.addExposedHeader("Access-Control-Allow-Origin"); config.addExposedHeader("Access-Control-Allow-Credentials"); - + config.addExposedHeader("X-Total-Count"); // 分页常用 + config.addExposedHeader("Link"); // HATEOAS 常用 // 设置预检请求的有效期(秒),从配置文件读取 config.setMaxAge(maxAge); diff --git a/src/main/java/com/qf/myafterprojecy/config/JwtAuthenticationFilter.java b/src/main/java/com/qf/myafterprojecy/config/JwtAuthenticationFilter.java index 413b357..7dc5a12 100644 --- a/src/main/java/com/qf/myafterprojecy/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/qf/myafterprojecy/config/JwtAuthenticationFilter.java @@ -1,10 +1,12 @@ package com.qf.myafterprojecy.config; import com.qf.myafterprojecy.utils.JwtUtils; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -40,19 +42,28 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private String tokenPrefix; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - try { - // 获取token - String token = getTokenFromRequest(request); - // System.out.println(token); - if (token != null) { - if (validateToken(token)) { - // 从token中获取用户名 - String username = jwtUtils.getUsernameFromToken(token); - // System.out.println("username: " + username); - // 加载用户信息 - UserDetails userDetails = userDetailsService.loadUserByUsername(username); + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + // 仅处理需要认证的请求,token为空时允许继续执行过滤器链 + String token = getTokenFromRequest(request); + // 如果没有token,继续执行过滤器链,让后续的过滤器或控制器来处理是否需要认证 + if (token == null) { + filterChain.doFilter(request, response); + return; + } + + try { + // 从token中获取用户名 + String username = jwtUtils.getUsernameFromToken(token); + + // 如果用户名不为空且当前上下文没有认证信息,则进行认证 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // 加载用户信息 + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + // 验证token + if (jwtUtils.validateToken(token, userDetails)) { // 创建认证对象 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); @@ -60,35 +71,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { // 设置认证信息到上下文 SecurityContextHolder.getContext().setAuthentication(authentication); - } else { - // 如果token无效但不为空,返回401 - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.getWriter().write("{\"message\":\"无效的认证令牌\"}"); - return; + logger.debug("用户 {} 认证成功", username); } } - } catch (io.jsonwebtoken.ExpiredJwtException e) { - // 专门处理令牌过期,返回401和明确的错误信息 - logger.error("令牌已过期: {}", e.getMessage()); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.getWriter().write("{\"message\":\"认证令牌已过期\"}"); + sendError(response, "令牌已过期", HttpServletResponse.SC_UNAUTHORIZED); return; - } catch (io.jsonwebtoken.JwtException e) { - // 其他JWT异常 - logger.error("无效的token格式: {}", e.getMessage()); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.getWriter().write("{\"message\":\"无效的令牌格式\"}"); + } catch (io.jsonwebtoken.JwtException | IllegalArgumentException e) { + sendError(response, "无效的令牌", HttpServletResponse.SC_UNAUTHORIZED); return; } catch (Exception e) { - logger.error("无法设置用户认证: {}", e); - SecurityContextHolder.clearContext(); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.getWriter().write("{\"message\":\"认证失败\"}"); + logger.error("JWT 认证未知错误", e); + sendError(response, "认证服务异常", HttpServletResponse.SC_UNAUTHORIZED); return; } @@ -101,32 +95,22 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private String getTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader(tokenHeader); if (bearerToken != null && bearerToken.startsWith(tokenPrefix + " ")) { - return bearerToken.substring(tokenPrefix.length() + 1); + return bearerToken.substring(tokenPrefix.length() + 1).trim(); } - + return null; } /** - * 验证token + * 发送未授权响应 */ - private boolean validateToken(String token) { - try { - String username = jwtUtils.getUsernameFromToken(token); - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - return jwtUtils.validateToken(token, userDetails); - } catch (io.jsonwebtoken.ExpiredJwtException e) { - // 专门处理令牌过期,记录日志但不抛出异常 - logger.error("令牌已过期: {}", e.getMessage()); - return false; - } catch (io.jsonwebtoken.JwtException e) { - // 其他JWT异常 - logger.error("无效的token格式: {}", e.getMessage()); - return false; - } catch (Exception e) { - // 其他异常 - logger.error("验证token失败: {}", e.getMessage()); - return false; - } + + private void sendError(HttpServletResponse response, String message, int status) throws IOException { + response.setStatus(status); + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(String.format("{\"message\":\"%s\"}", message)); } } diff --git a/src/main/java/com/qf/myafterprojecy/config/SecurityConfig.java b/src/main/java/com/qf/myafterprojecy/config/SecurityConfig.java index 83e5b15..eda347e 100644 --- a/src/main/java/com/qf/myafterprojecy/config/SecurityConfig.java +++ b/src/main/java/com/qf/myafterprojecy/config/SecurityConfig.java @@ -11,8 +11,6 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.http.HttpServletResponse; @@ -27,12 +25,6 @@ import javax.servlet.http.HttpServletResponse; @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class SecurityConfig { - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @@ -52,32 +44,22 @@ public class SecurityConfig { * @throws Exception 配置过程中可能出现的异常 */ @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChain( HttpSecurity http) throws Exception { http // 禁用CSRF保护(对于API服务通常不需要) .csrf().disable() // 使用自定义的CORS过滤器,不使用默认的cors()配置 // 配置URL访问权限 - .authorizeRequests() - // 允许公开访问的路径 - // 登录和认证相关端点应该全部公开 - - - .antMatchers(HttpMethod.POST,"/api/auth/**").permitAll() - // 文章浏览量增加接口公开 - .antMatchers(HttpMethod.POST,"/api/articles/view/**").permitAll() - // 所有GET请求公开 - .antMatchers(HttpMethod.GET,"/api/**").permitAll() - // 公开评论新增接口 - .antMatchers(HttpMethod.POST,"/api/messages").permitAll() - // 新增、删除、修改操作需要管理员权限 - .antMatchers(HttpMethod.POST,"/api/**").hasRole("ADMIN") - .antMatchers(HttpMethod.PUT,"/api/**").hasRole("ADMIN") - .antMatchers(HttpMethod.DELETE,"/api/**").hasRole("ADMIN") - // 管理员才能访问的路径 - .antMatchers("/api/admin/**").hasRole("ADMIN") - // 其他所有请求都需要认证 - .anyRequest().authenticated() + .authorizeRequests() + .antMatchers(HttpMethod.POST, "/api/auth/**").permitAll() + .antMatchers(HttpMethod.POST, "/api/articles/view/**").permitAll() + .antMatchers(HttpMethod.POST, "/api/messages").permitAll() // ← 放在通用 POST 之前 + .antMatchers(HttpMethod.GET, "/api/**").permitAll() + .antMatchers(HttpMethod.POST, "/api/**").hasRole("ADMIN") // ← 通用规则放最后 + .antMatchers(HttpMethod.PUT, "/api/**").hasRole("ADMIN") + .antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN") + .antMatchers("/api/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() .and() // 配置会话管理,使用无状态会话策略 // 这意味着每个请求都需要包含认证信息(如JWT) @@ -104,12 +86,6 @@ public class SecurityConfig { // 确保Spring Security不会添加额外的CharacterEncodingFilter // 因为我们在CharacterEncodingConfig中已经配置了自定义的过滤器 - http.addFilterBefore((request, response, chain) -> { - // 确保响应使用UTF-8编码 - response.setCharacterEncoding("UTF-8"); - response.setContentType("application/json;charset=UTF-8"); - chain.doFilter(request, response); - }, JwtAuthenticationFilter.class); // 配置访问拒绝处理器 http.exceptionHandling() diff --git a/src/main/java/com/qf/myafterprojecy/controller/AuthController.java b/src/main/java/com/qf/myafterprojecy/controller/AuthController.java index bd6e352..989ad1a 100644 --- a/src/main/java/com/qf/myafterprojecy/controller/AuthController.java +++ b/src/main/java/com/qf/myafterprojecy/controller/AuthController.java @@ -11,14 +11,18 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import javax.servlet.http.HttpServletRequest; + /** * 认证控制器 * 处理用户登录相关请求 @@ -62,23 +66,25 @@ public class AuthController { /** * 用户登录接口 + * * @param loginRequest 登录请求参数 * @return 登录结果 */ @PostMapping("/login") public ResponseMessage> login(@RequestBody LoginRequest loginRequest) { logger.info("用户登录请求: {}", loginRequest.getUsername()); - + try { // 创建认证令牌 - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); - + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginRequest.getUsername(), loginRequest.getPassword()); + // 执行认证 Authentication authentication = authenticationManager.authenticate(authenticationToken); - + // 将认证信息存入上下文 SecurityContextHolder.getContext().setAuthentication(authentication); - + // 获取认证后的用户信息 UserDetails userDetails = (UserDetails) authentication.getPrincipal(); // 生成JWT token @@ -89,9 +95,9 @@ public class AuthController { data.put("authorities", userDetails.getAuthorities()); data.put("token", token); data.put("tokenPrefix", jwtUtils.getTokenPrefix()); - + return ResponseMessage.success(data, "登录成功"); - + } catch (AuthenticationException e) { logger.error("登录失败: {}", e.getMessage()); return ResponseMessage.error("用户名或密码错误"); @@ -100,26 +106,28 @@ public class AuthController { /** * 获取当前登录用户信息 + * * @return 当前用户信息 */ @PostMapping("/info") public ResponseMessage> getCurrentUserInfo() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - + if (authentication == null || !authentication.isAuthenticated()) { return ResponseMessage.error("未登录"); } - + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); Map data = new HashMap<>(); data.put("username", userDetails.getUsername()); data.put("authorities", userDetails.getAuthorities()); - + return ResponseMessage.success(data, "获取用户信息成功"); } /** * 用户登出接口 + * * @return 登出结果 */ @PostMapping("/logout") @@ -127,4 +135,11 @@ public class AuthController { SecurityContextHolder.clearContext(); return ResponseMessage.successEmpty("登出成功"); } + + @GetMapping("/debug") + public Map debug(HttpServletRequest request) { + return Collections.singletonMap( + "Authorization", + request.getHeader("Authorization")); + } } diff --git a/src/main/java/com/qf/myafterprojecy/init/UserDataInit.java b/src/main/java/com/qf/myafterprojecy/init/UserDataInit.java deleted file mode 100644 index 0d72374..0000000 --- a/src/main/java/com/qf/myafterprojecy/init/UserDataInit.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.qf.myafterprojecy.init; - -import com.qf.myafterprojecy.pojo.Users; -import com.qf.myafterprojecy.repository.UsersRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; -import java.util.Optional; - -/** - * 用户数据初始化类 - * 在应用启动时检查并创建管理员账号 - */ -@Component -public class UserDataInit implements ApplicationRunner { - - private static final Logger logger = LoggerFactory.getLogger(UserDataInit.class); - - @Autowired - private UsersRepository usersRepository; - - @Autowired - private PasswordEncoder passwordEncoder; - - // 管理员账号信息 - private static final String ADMIN_USERNAME = "qf1121"; - private static final String ADMIN_PASSWORD = "qf1121"; - private static final String ADMIN_EMAIL = "admin@qf1121.com"; - private static final String ADMIN_PHONE = "13800138000"; - private static final Integer ADMIN_ROLE = 1; // 1表示管理员角色 - - @Override - public void run(ApplicationArguments args) { - logger.info("开始检查管理员账号..."); - - // 检查管理员账号是否已存在 - Optional adminUser = usersRepository.findByUsername(ADMIN_USERNAME); - - if (adminUser.isPresent()) { - logger.info("管理员账号 {} 已存在,无需创建", ADMIN_USERNAME); - } else { - // 创建管理员账号 - Users newAdmin = new Users(); - newAdmin.setUsername(ADMIN_USERNAME); - // 加密密码 - newAdmin.setPassword(passwordEncoder.encode(ADMIN_PASSWORD)); - newAdmin.setEmail(ADMIN_EMAIL); - newAdmin.setPhone(ADMIN_PHONE); - newAdmin.setRole(ADMIN_ROLE); - newAdmin.setCreateTime(LocalDateTime.now()); - try { - usersRepository.save(newAdmin); - logger.info("管理员账号 {} 创建成功", ADMIN_USERNAME); - } catch (Exception e) { - logger.error("创建管理员账号失败: {}", e.getMessage()); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/qf/myafterprojecy/pojo/Nonsense.java b/src/main/java/com/qf/myafterprojecy/pojo/Nonsense.java index 287c147..8012cff 100644 --- a/src/main/java/com/qf/myafterprojecy/pojo/Nonsense.java +++ b/src/main/java/com/qf/myafterprojecy/pojo/Nonsense.java @@ -19,6 +19,7 @@ public class Nonsense { @Column(name = "time") private Date time; + public Integer getId() { return id; } diff --git a/src/main/java/com/qf/myafterprojecy/pojo/dto/CategoryAttributeDto.java b/src/main/java/com/qf/myafterprojecy/pojo/dto/CategoryAttributeDto.java index a46238c..24641cf 100644 --- a/src/main/java/com/qf/myafterprojecy/pojo/dto/CategoryAttributeDto.java +++ b/src/main/java/com/qf/myafterprojecy/pojo/dto/CategoryAttributeDto.java @@ -2,7 +2,6 @@ package com.qf.myafterprojecy.pojo.dto; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; - public class CategoryAttributeDto { private Integer attributeid; @@ -11,6 +10,8 @@ public class CategoryAttributeDto { @NotBlank(message = "属性名称不能为空") private String attributename; + + private Integer articlecount; // Getters and Setters public Integer getAttributeid() { @@ -36,4 +37,12 @@ public class CategoryAttributeDto { public void setAttributename(String attributename) { this.attributename = attributename; } + + public Integer getArticlecount() { + return articlecount; + } + + public void setArticlecount(Integer articlecount) { + this.articlecount = articlecount; + } } \ No newline at end of file diff --git a/src/main/java/com/qf/myafterprojecy/pojo/dto/CategoryTreeDto.java b/src/main/java/com/qf/myafterprojecy/pojo/dto/CategoryTreeDto.java index b0cf519..0eb9035 100644 --- a/src/main/java/com/qf/myafterprojecy/pojo/dto/CategoryTreeDto.java +++ b/src/main/java/com/qf/myafterprojecy/pojo/dto/CategoryTreeDto.java @@ -4,16 +4,17 @@ import java.util.List; import com.qf.myafterprojecy.pojo.Categoryattribute; + public class CategoryTreeDto { private Integer id; private String name; - private List children; + private List children; // 构造方法 public CategoryTreeDto() { } // 全参构造方法 - public CategoryTreeDto(Integer id, String name, List children) { + public CategoryTreeDto(Integer id, String name, List children) { this.id = id; this.name = name; this.children = children; @@ -31,10 +32,10 @@ public class CategoryTreeDto { public void setName(String name) { this.name = name; } - public List getChildren() { + public List getChildren() { return children; } - public void setChildren(List children) { + public void setChildren(List children) { this.children = children; } } \ No newline at end of file diff --git a/src/main/java/com/qf/myafterprojecy/repository/ArticleRepository.java b/src/main/java/com/qf/myafterprojecy/repository/ArticleRepository.java index bb3e075..38cb8ad 100644 --- a/src/main/java/com/qf/myafterprojecy/repository/ArticleRepository.java +++ b/src/main/java/com/qf/myafterprojecy/repository/ArticleRepository.java @@ -18,11 +18,11 @@ public interface ArticleRepository extends JpaRepository { * 根据文章ID查询文章信息的方法 * 使用JPQL(Java Persistence Query Language)进行查询 * - * @param id 文章的唯一标识符,作为查询条件 + * @param articleid 文章的唯一标识符,作为查询条件 * @return 返回一个Optional
对象,可能包含文章信息,也可能为空(如果未找到对应ID的文章) */ - @Query("SELECT a FROM Article a WHERE a.articleid = :id") - Optional
findById(@Param("id") Integer id); + @Query("SELECT a FROM Article a WHERE a.articleid = :articleid") + Optional
findById(@Param("articleid") Integer articleid); /** * 根据标题查询文章列表 @@ -132,4 +132,7 @@ public interface ArticleRepository extends JpaRepository { */ @Query("SELECT COUNT(a) FROM Article a WHERE a.status = :status") Integer countByStatus(@Param("status") Integer status); + // 统计指定属性ID的文章数量 + @Query("SELECT COUNT(a) FROM Article a WHERE a.status = :status AND a.attributeid = :attributeid") + Integer countByStatusAndAttributeid(@Param("status") Integer status, @Param("attributeid") Integer attributeid); } diff --git a/src/main/java/com/qf/myafterprojecy/repository/MessageRepository.java b/src/main/java/com/qf/myafterprojecy/repository/MessageRepository.java index cc43ef6..ace6a47 100644 --- a/src/main/java/com/qf/myafterprojecy/repository/MessageRepository.java +++ b/src/main/java/com/qf/myafterprojecy/repository/MessageRepository.java @@ -2,7 +2,6 @@ package com.qf.myafterprojecy.repository; import com.qf.myafterprojecy.pojo.Message; -import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties.Pageable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/qf/myafterprojecy/service/IArticleService.java b/src/main/java/com/qf/myafterprojecy/service/IArticleService.java index f2055ab..6c94d9c 100644 --- a/src/main/java/com/qf/myafterprojecy/service/IArticleService.java +++ b/src/main/java/com/qf/myafterprojecy/service/IArticleService.java @@ -6,8 +6,6 @@ import com.qf.myafterprojecy.pojo.dto.ArriclePageDto; import com.qf.myafterprojecy.pojo.dto.ArticleDto; import com.qf.myafterprojecy.pojo.dto.ArticleTreeDto; -import org.springframework.data.domain.Page; - import java.util.List; public interface IArticleService { diff --git a/src/main/java/com/qf/myafterprojecy/service/impl/CategoryService.java b/src/main/java/com/qf/myafterprojecy/service/impl/CategoryService.java index 54f14fb..2dfa02c 100644 --- a/src/main/java/com/qf/myafterprojecy/service/impl/CategoryService.java +++ b/src/main/java/com/qf/myafterprojecy/service/impl/CategoryService.java @@ -2,8 +2,11 @@ package com.qf.myafterprojecy.service.impl; import com.qf.myafterprojecy.exceptopn.ResponseMessage; import com.qf.myafterprojecy.pojo.Category; +import com.qf.myafterprojecy.pojo.Categoryattribute; +import com.qf.myafterprojecy.pojo.dto.CategoryAttributeDto; import com.qf.myafterprojecy.pojo.dto.CategoryDto; import com.qf.myafterprojecy.pojo.dto.CategoryTreeDto; +import com.qf.myafterprojecy.repository.ArticleRepository; import com.qf.myafterprojecy.repository.CategoryAttributeRepository; import com.qf.myafterprojecy.repository.CategoryRepository; import com.qf.myafterprojecy.service.ICategoryService; @@ -30,6 +33,8 @@ public class CategoryService implements ICategoryService { private CategoryRepository categoryRepository; @Autowired private CategoryAttributeRepository categoryAttributeRepository; + @Autowired + private ArticleRepository articleRepository; @Override @Transactional(readOnly = true) @@ -185,8 +190,17 @@ public class CategoryService implements ICategoryService { CategoryTreeDto node = new CategoryTreeDto(); node.setId(category.getCategoryid()); node.setName(category.getTypename()); - - node.setChildren(categoryAttributeRepository.findByCategoryId(category.getCategoryid())); + List categoryAttributes = categoryAttributeRepository.findByCategoryId(category.getCategoryid()); + List categoryAttributeDtos = categoryAttributes.stream() + .map(attr -> { + CategoryAttributeDto dto = new CategoryAttributeDto(); + dto.setAttributeid(attr.getAttributeid()); + dto.setCategoryid(attr.getCategoryid()); + dto.setAttributename(attr.getAttributename()); + dto.setArticlecount(articleRepository.countByStatusAndAttributeid(1, attr.getAttributeid())); + return dto; + }).collect(Collectors.toList()); + node.setChildren(categoryAttributeDtos); return node; } } \ No newline at end of file diff --git a/src/main/java/com/qf/myafterprojecy/service/impl/MessageService.java b/src/main/java/com/qf/myafterprojecy/service/impl/MessageService.java index db40938..6fbed6a 100644 --- a/src/main/java/com/qf/myafterprojecy/service/impl/MessageService.java +++ b/src/main/java/com/qf/myafterprojecy/service/impl/MessageService.java @@ -16,7 +16,6 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Comparator; @@ -81,12 +80,12 @@ public class MessageService implements IMessageService { } // 业务逻辑校验 - if (StringUtils.isEmpty(messageDto.getContent())) { + if ((messageDto.getContent() == null || messageDto.getContent().trim().isEmpty())) { logger.warn("保存消息时内容为空"); throw new IllegalArgumentException("Message content cannot be empty"); } - if (StringUtils.isEmpty(messageDto.getNickname())) { + if ((messageDto.getNickname() == null || messageDto.getNickname().trim().isEmpty())) { logger.warn("保存消息时昵称为空"); throw new IllegalArgumentException("Message nickname cannot be empty"); } @@ -212,7 +211,7 @@ public class MessageService implements IMessageService { @Override public ResponseMessage> searchMessagesByNickname(String nickname) { - if (StringUtils.isEmpty(nickname)) { + if ((nickname == null || nickname.trim().isEmpty())) { logger.warn("根据昵称查询消息时昵称为空"); return ResponseMessage.badRequest("昵称不能为空"); } @@ -414,7 +413,6 @@ public class MessageService implements IMessageService { } return count; } - /** * 递归按时间排序子节点 */ diff --git a/src/main/java/com/qf/myafterprojecy/service/impl/NonsenseService.java b/src/main/java/com/qf/myafterprojecy/service/impl/NonsenseService.java index 8b36a30..b3c1a67 100644 --- a/src/main/java/com/qf/myafterprojecy/service/impl/NonsenseService.java +++ b/src/main/java/com/qf/myafterprojecy/service/impl/NonsenseService.java @@ -11,7 +11,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties.Pageable; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; diff --git a/src/main/resources/META-INF/spring-devtools.properties b/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..6d61e78 --- /dev/null +++ b/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1,5 @@ +# Include JJWT library in restart classloader to prevent ClassNotFoundException +restart.include.jjwt=/jjwt-[\w\d\.\-]+.jar +restart.include.jjwt-api=/jjwt-api-[\w\d\.\-]+.jar +restart.include.jjwt-impl=/jjwt-impl-[\w\d\.\-]+.jar +restart.include.jjwt-jackson=/jjwt-jackson-[\w\d\.\-]+.jar diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 53cc794..402bec6 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -38,10 +38,11 @@ jwt.token-prefix=Bearer # 安全与CORS配置 - 开发用 # ==================================================================== # CORS配置(开发环境允许所有本地前端访问) -cors.allowed-origins=http://localhost:3000,http://localhost:8080,http://localhost:5173 +cors.allowed-origins=http://localhost:3000,http://localhost:8080,http://localhost:5173,http://localhost:5174 cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS cors.allowed-headers=* cors.allow-credentials=true +cors.max-age=3600 # ==================================================================== # 日志配置 - 开发用(详细日志便于调试) diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 18f7117..2cd8359 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -37,7 +37,7 @@ spring.jpa.properties.hibernate.order_updates=true # ==================================================================== # JWT 配置 - 生产用(敏感信息从环境变量读取) # ==================================================================== -jwt.secret=${JWT_SECRET:6a1f4832-29bf-4ac5-9408-a8813b6f2dfe} +jwt.secret=${JWT_SECRET:mySecretKey123} jwt.expiration=${JWT_EXPIRATION:3600000} jwt.header=Authorization jwt.token-prefix=Bearer