feat: 优化JWT认证过滤器并增强跨域配置

refactor: 重构安全配置和认证控制器逻辑

fix: 修正消息服务中的空值检查逻辑

feat(article): 为分类属性添加文章计数功能

chore: 删除不再使用的初始化类和字符编码配置

style: 清理代码中的无用导入和空白行

perf(cors): 添加开发环境跨域配置和响应头

docs: 更新JWT配置注释和默认密钥

test: 移除Spring Data Web分页属性导入

ci: 添加spring-devtools配置支持JJWT库
This commit is contained in:
qingfeng1121
2025-12-25 13:25:25 +08:00
parent d679661fac
commit fe3bff2642
18 changed files with 128 additions and 238 deletions

View File

@@ -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<Filter> 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<Filter> registrationBean = new FilterRegistrationBean<>(encodingFilter);
// 设置过滤器顺序为最高优先级
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
// 为所有请求路径注册过滤器
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}

View File

@@ -26,10 +26,9 @@ public class CorsConfig {
@Value("${cors.allow-credentials}") @Value("${cors.allow-credentials}")
private Boolean allowCredentials; private Boolean allowCredentials;
@Value("${cors.max-age:3600}") @Value("${cors.max-age}")
private Long maxAge; private Long maxAge;
/** /**
* 创建CORS过滤器配置跨域请求的规则 * 创建CORS过滤器配置跨域请求的规则
* 从配置文件中读取CORS配置实现配置的统一管理 * 从配置文件中读取CORS配置实现配置的统一管理
@@ -64,7 +63,8 @@ public class CorsConfig {
config.addExposedHeader("Accept"); config.addExposedHeader("Accept");
config.addExposedHeader("Access-Control-Allow-Origin"); config.addExposedHeader("Access-Control-Allow-Origin");
config.addExposedHeader("Access-Control-Allow-Credentials"); config.addExposedHeader("Access-Control-Allow-Credentials");
config.addExposedHeader("X-Total-Count"); // 分页常用
config.addExposedHeader("Link"); // HATEOAS 常用
// 设置预检请求的有效期(秒),从配置文件读取 // 设置预检请求的有效期(秒),从配置文件读取
config.setMaxAge(maxAge); config.setMaxAge(maxAge);

View File

@@ -1,10 +1,12 @@
package com.qf.myafterprojecy.config; package com.qf.myafterprojecy.config;
import com.qf.myafterprojecy.utils.JwtUtils; import com.qf.myafterprojecy.utils.JwtUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@@ -40,19 +42,28 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private String tokenPrefix; private String tokenPrefix;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
try { @NonNull FilterChain filterChain) throws ServletException, IOException {
// 获取token // 仅处理需要认证的请求token为空时允许继续执行过滤器链
String token = getTokenFromRequest(request); 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);
// 如果没有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( UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()); userDetails, null, userDetails.getAuthorities());
@@ -60,35 +71,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 设置认证信息到上下文 // 设置认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
} else { logger.debug("用户 {} 认证成功", username);
// 如果token无效但不为空返回401
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"无效的认证令牌\"}");
return;
} }
} }
} catch (io.jsonwebtoken.ExpiredJwtException e) { } catch (io.jsonwebtoken.ExpiredJwtException e) {
// 专门处理令牌过期返回401和明确的错误信息 sendError(response, "令牌过期", HttpServletResponse.SC_UNAUTHORIZED);
logger.error("令牌已过期: {}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"认证令牌已过期\"}");
return; return;
} catch (io.jsonwebtoken.JwtException e) { } catch (io.jsonwebtoken.JwtException | IllegalArgumentException e) {
// 其他JWT异常 sendError(response, "无效的令牌", HttpServletResponse.SC_UNAUTHORIZED);
logger.error("无效的token格式: {}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"无效的令牌格式\"}");
return; return;
} catch (Exception e) { } catch (Exception e) {
logger.error("无法设置用户认证: {}", e); logger.error("JWT 认证未知错误", e);
SecurityContextHolder.clearContext(); sendError(response, "认证服务异常", HttpServletResponse.SC_UNAUTHORIZED);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"认证失败\"}");
return; return;
} }
@@ -101,32 +95,22 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private String getTokenFromRequest(HttpServletRequest request) { private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(tokenHeader); String bearerToken = request.getHeader(tokenHeader);
if (bearerToken != null && bearerToken.startsWith(tokenPrefix + " ")) { if (bearerToken != null && bearerToken.startsWith(tokenPrefix + " ")) {
return bearerToken.substring(tokenPrefix.length() + 1); return bearerToken.substring(tokenPrefix.length() + 1).trim();
} }
return null; return null;
} }
/** /**
* 验证token * 发送未授权响应
*/ */
private boolean validateToken(String token) {
try { private void sendError(HttpServletResponse response, String message, int status) throws IOException {
String username = jwtUtils.getUsernameFromToken(token); response.setStatus(status);
UserDetails userDetails = userDetailsService.loadUserByUsername(username); response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
return jwtUtils.validateToken(token, userDetails); response.setHeader("Pragma", "no-cache");
} catch (io.jsonwebtoken.ExpiredJwtException e) { response.setDateHeader("Expires", 0);
// 专门处理令牌过期,记录日志但不抛出异常 response.setContentType("application/json;charset=UTF-8");
logger.error("令牌已过期: {}", e.getMessage()); response.getWriter().write(String.format("{\"message\":\"%s\"}", message));
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;
}
} }
} }

View File

@@ -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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; 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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -27,12 +25,6 @@ import javax.servlet.http.HttpServletResponse;
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig { public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired @Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter; private JwtAuthenticationFilter jwtAuthenticationFilter;
@@ -52,32 +44,22 @@ public class SecurityConfig {
* @throws Exception 配置过程中可能出现的异常 * @throws Exception 配置过程中可能出现的异常
*/ */
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain( HttpSecurity http) throws Exception {
http http
// 禁用CSRF保护对于API服务通常不需要 // 禁用CSRF保护对于API服务通常不需要
.csrf().disable() .csrf().disable()
// 使用自定义的CORS过滤器不使用默认的cors()配置 // 使用自定义的CORS过滤器不使用默认的cors()配置
// 配置URL访问权限 // 配置URL访问权限
.authorizeRequests() .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/auth/**").permitAll() .antMatchers(HttpMethod.POST, "/api/**").hasRole("ADMIN") // ← 通用规则放最后
// 文章浏览量增加接口公开 .antMatchers(HttpMethod.PUT, "/api/**").hasRole("ADMIN")
.antMatchers(HttpMethod.POST,"/api/articles/view/**").permitAll() .antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
// 所有GET请求公开 .antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers(HttpMethod.GET,"/api/**").permitAll() .anyRequest().authenticated()
// 公开评论新增接口
.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()
.and() .and()
// 配置会话管理,使用无状态会话策略 // 配置会话管理,使用无状态会话策略
// 这意味着每个请求都需要包含认证信息如JWT // 这意味着每个请求都需要包含认证信息如JWT
@@ -104,12 +86,6 @@ public class SecurityConfig {
// 确保Spring Security不会添加额外的CharacterEncodingFilter // 确保Spring Security不会添加额外的CharacterEncodingFilter
// 因为我们在CharacterEncodingConfig中已经配置了自定义的过滤器 // 因为我们在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() http.exceptionHandling()

View File

@@ -11,14 +11,18 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; 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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.servlet.http.HttpServletRequest;
/** /**
* 认证控制器 * 认证控制器
* 处理用户登录相关请求 * 处理用户登录相关请求
@@ -62,23 +66,25 @@ public class AuthController {
/** /**
* 用户登录接口 * 用户登录接口
*
* @param loginRequest 登录请求参数 * @param loginRequest 登录请求参数
* @return 登录结果 * @return 登录结果
*/ */
@PostMapping("/login") @PostMapping("/login")
public ResponseMessage<Map<String, Object>> login(@RequestBody LoginRequest loginRequest) { public ResponseMessage<Map<String, Object>> login(@RequestBody LoginRequest loginRequest) {
logger.info("用户登录请求: {}", loginRequest.getUsername()); logger.info("用户登录请求: {}", loginRequest.getUsername());
try { try {
// 创建认证令牌 // 创建认证令牌
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getPassword());
// 执行认证 // 执行认证
Authentication authentication = authenticationManager.authenticate(authenticationToken); Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 将认证信息存入上下文 // 将认证信息存入上下文
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
// 获取认证后的用户信息 // 获取认证后的用户信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 生成JWT token // 生成JWT token
@@ -89,9 +95,9 @@ public class AuthController {
data.put("authorities", userDetails.getAuthorities()); data.put("authorities", userDetails.getAuthorities());
data.put("token", token); data.put("token", token);
data.put("tokenPrefix", jwtUtils.getTokenPrefix()); data.put("tokenPrefix", jwtUtils.getTokenPrefix());
return ResponseMessage.success(data, "登录成功"); return ResponseMessage.success(data, "登录成功");
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
logger.error("登录失败: {}", e.getMessage()); logger.error("登录失败: {}", e.getMessage());
return ResponseMessage.error("用户名或密码错误"); return ResponseMessage.error("用户名或密码错误");
@@ -100,26 +106,28 @@ public class AuthController {
/** /**
* 获取当前登录用户信息 * 获取当前登录用户信息
*
* @return 当前用户信息 * @return 当前用户信息
*/ */
@PostMapping("/info") @PostMapping("/info")
public ResponseMessage<Map<String, Object>> getCurrentUserInfo() { public ResponseMessage<Map<String, Object>> getCurrentUserInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
return ResponseMessage.error("未登录"); return ResponseMessage.error("未登录");
} }
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("username", userDetails.getUsername()); data.put("username", userDetails.getUsername());
data.put("authorities", userDetails.getAuthorities()); data.put("authorities", userDetails.getAuthorities());
return ResponseMessage.success(data, "获取用户信息成功"); return ResponseMessage.success(data, "获取用户信息成功");
} }
/** /**
* 用户登出接口 * 用户登出接口
*
* @return 登出结果 * @return 登出结果
*/ */
@PostMapping("/logout") @PostMapping("/logout")
@@ -127,4 +135,11 @@ public class AuthController {
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
return ResponseMessage.successEmpty("登出成功"); return ResponseMessage.successEmpty("登出成功");
} }
@GetMapping("/debug")
public Map<String, String> debug(HttpServletRequest request) {
return Collections.singletonMap(
"Authorization",
request.getHeader("Authorization"));
}
} }

View File

@@ -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<Users> 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());
}
}
}
}

View File

@@ -19,6 +19,7 @@ public class Nonsense {
@Column(name = "time") @Column(name = "time")
private Date time; private Date time;
public Integer getId() { public Integer getId() {
return id; return id;
} }

View File

@@ -2,7 +2,6 @@ package com.qf.myafterprojecy.pojo.dto;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
public class CategoryAttributeDto { public class CategoryAttributeDto {
private Integer attributeid; private Integer attributeid;
@@ -11,6 +10,8 @@ public class CategoryAttributeDto {
@NotBlank(message = "属性名称不能为空") @NotBlank(message = "属性名称不能为空")
private String attributename; private String attributename;
private Integer articlecount;
// Getters and Setters // Getters and Setters
public Integer getAttributeid() { public Integer getAttributeid() {
@@ -36,4 +37,12 @@ public class CategoryAttributeDto {
public void setAttributename(String attributename) { public void setAttributename(String attributename) {
this.attributename = attributename; this.attributename = attributename;
} }
public Integer getArticlecount() {
return articlecount;
}
public void setArticlecount(Integer articlecount) {
this.articlecount = articlecount;
}
} }

View File

@@ -4,16 +4,17 @@ import java.util.List;
import com.qf.myafterprojecy.pojo.Categoryattribute; import com.qf.myafterprojecy.pojo.Categoryattribute;
public class CategoryTreeDto { public class CategoryTreeDto {
private Integer id; private Integer id;
private String name; private String name;
private List<Categoryattribute> children; private List<CategoryAttributeDto> children;
// 构造方法 // 构造方法
public CategoryTreeDto() { public CategoryTreeDto() {
} }
// 全参构造方法 // 全参构造方法
public CategoryTreeDto(Integer id, String name, List<Categoryattribute> children) { public CategoryTreeDto(Integer id, String name, List<CategoryAttributeDto> children) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.children = children; this.children = children;
@@ -31,10 +32,10 @@ public class CategoryTreeDto {
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
public List<Categoryattribute> getChildren() { public List<CategoryAttributeDto> getChildren() {
return children; return children;
} }
public void setChildren(List<Categoryattribute> children) { public void setChildren(List<CategoryAttributeDto> children) {
this.children = children; this.children = children;
} }
} }

View File

@@ -18,11 +18,11 @@ public interface ArticleRepository extends JpaRepository<Article, Integer> {
* 根据文章ID查询文章信息的方法 * 根据文章ID查询文章信息的方法
* 使用JPQLJava Persistence Query Language进行查询 * 使用JPQLJava Persistence Query Language进行查询
* *
* @param id 文章的唯一标识符,作为查询条件 * @param articleid 文章的唯一标识符,作为查询条件
* @return 返回一个Optional<Article>对象可能包含文章信息也可能为空如果未找到对应ID的文章 * @return 返回一个Optional<Article>对象可能包含文章信息也可能为空如果未找到对应ID的文章
*/ */
@Query("SELECT a FROM Article a WHERE a.articleid = :id") @Query("SELECT a FROM Article a WHERE a.articleid = :articleid")
Optional<Article> findById(@Param("id") Integer id); Optional<Article> findById(@Param("articleid") Integer articleid);
/** /**
* 根据标题查询文章列表 * 根据标题查询文章列表
@@ -132,4 +132,7 @@ public interface ArticleRepository extends JpaRepository<Article, Integer> {
*/ */
@Query("SELECT COUNT(a) FROM Article a WHERE a.status = :status") @Query("SELECT COUNT(a) FROM Article a WHERE a.status = :status")
Integer countByStatus(@Param("status") Integer 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);
} }

View File

@@ -2,7 +2,6 @@ package com.qf.myafterprojecy.repository;
import com.qf.myafterprojecy.pojo.Message; 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.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;

View File

@@ -6,8 +6,6 @@ import com.qf.myafterprojecy.pojo.dto.ArriclePageDto;
import com.qf.myafterprojecy.pojo.dto.ArticleDto; import com.qf.myafterprojecy.pojo.dto.ArticleDto;
import com.qf.myafterprojecy.pojo.dto.ArticleTreeDto; import com.qf.myafterprojecy.pojo.dto.ArticleTreeDto;
import org.springframework.data.domain.Page;
import java.util.List; import java.util.List;
public interface IArticleService { public interface IArticleService {

View File

@@ -2,8 +2,11 @@ package com.qf.myafterprojecy.service.impl;
import com.qf.myafterprojecy.exceptopn.ResponseMessage; import com.qf.myafterprojecy.exceptopn.ResponseMessage;
import com.qf.myafterprojecy.pojo.Category; 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.CategoryDto;
import com.qf.myafterprojecy.pojo.dto.CategoryTreeDto; import com.qf.myafterprojecy.pojo.dto.CategoryTreeDto;
import com.qf.myafterprojecy.repository.ArticleRepository;
import com.qf.myafterprojecy.repository.CategoryAttributeRepository; import com.qf.myafterprojecy.repository.CategoryAttributeRepository;
import com.qf.myafterprojecy.repository.CategoryRepository; import com.qf.myafterprojecy.repository.CategoryRepository;
import com.qf.myafterprojecy.service.ICategoryService; import com.qf.myafterprojecy.service.ICategoryService;
@@ -30,6 +33,8 @@ public class CategoryService implements ICategoryService {
private CategoryRepository categoryRepository; private CategoryRepository categoryRepository;
@Autowired @Autowired
private CategoryAttributeRepository categoryAttributeRepository; private CategoryAttributeRepository categoryAttributeRepository;
@Autowired
private ArticleRepository articleRepository;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -185,8 +190,17 @@ public class CategoryService implements ICategoryService {
CategoryTreeDto node = new CategoryTreeDto(); CategoryTreeDto node = new CategoryTreeDto();
node.setId(category.getCategoryid()); node.setId(category.getCategoryid());
node.setName(category.getTypename()); node.setName(category.getTypename());
List<Categoryattribute> categoryAttributes = categoryAttributeRepository.findByCategoryId(category.getCategoryid());
node.setChildren(categoryAttributeRepository.findByCategoryId(category.getCategoryid())); List<CategoryAttributeDto> 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; return node;
} }
} }

View File

@@ -16,7 +16,6 @@ import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; 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("保存消息时内容为空"); logger.warn("保存消息时内容为空");
throw new IllegalArgumentException("Message content cannot be empty"); throw new IllegalArgumentException("Message content cannot be empty");
} }
if (StringUtils.isEmpty(messageDto.getNickname())) { if ((messageDto.getNickname() == null || messageDto.getNickname().trim().isEmpty())) {
logger.warn("保存消息时昵称为空"); logger.warn("保存消息时昵称为空");
throw new IllegalArgumentException("Message nickname cannot be empty"); throw new IllegalArgumentException("Message nickname cannot be empty");
} }
@@ -212,7 +211,7 @@ public class MessageService implements IMessageService {
@Override @Override
public ResponseMessage<List<Message>> searchMessagesByNickname(String nickname) { public ResponseMessage<List<Message>> searchMessagesByNickname(String nickname) {
if (StringUtils.isEmpty(nickname)) { if ((nickname == null || nickname.trim().isEmpty())) {
logger.warn("根据昵称查询消息时昵称为空"); logger.warn("根据昵称查询消息时昵称为空");
return ResponseMessage.badRequest("昵称不能为空"); return ResponseMessage.badRequest("昵称不能为空");
} }
@@ -414,7 +413,6 @@ public class MessageService implements IMessageService {
} }
return count; return count;
} }
/** /**
* 递归按时间排序子节点 * 递归按时间排序子节点
*/ */

View File

@@ -11,7 +11,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties.Pageable;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;

View File

@@ -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

View File

@@ -38,10 +38,11 @@ jwt.token-prefix=Bearer
# 安全与CORS配置 - 开发用 # 安全与CORS配置 - 开发用
# ==================================================================== # ====================================================================
# 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-methods=GET,POST,PUT,DELETE,OPTIONS
cors.allowed-headers=* cors.allowed-headers=*
cors.allow-credentials=true cors.allow-credentials=true
cors.max-age=3600
# ==================================================================== # ====================================================================
# 日志配置 - 开发用(详细日志便于调试) # 日志配置 - 开发用(详细日志便于调试)

View File

@@ -37,7 +37,7 @@ spring.jpa.properties.hibernate.order_updates=true
# ==================================================================== # ====================================================================
# JWT 配置 - 生产用(敏感信息从环境变量读取) # JWT 配置 - 生产用(敏感信息从环境变量读取)
# ==================================================================== # ====================================================================
jwt.secret=${JWT_SECRET:6a1f4832-29bf-4ac5-9408-a8813b6f2dfe} jwt.secret=${JWT_SECRET:mySecretKey123}
jwt.expiration=${JWT_EXPIRATION:3600000} jwt.expiration=${JWT_EXPIRATION:3600000}
jwt.header=Authorization jwt.header=Authorization
jwt.token-prefix=Bearer jwt.token-prefix=Bearer