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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
// 仅处理需要认证的请求,token为空时允许继续执行过滤器链
|
||||
String token = getTokenFromRequest(request);
|
||||
// System.out.println(token);
|
||||
if (token != null) {
|
||||
if (validateToken(token)) {
|
||||
|
||||
// 如果没有token,继续执行过滤器链,让后续的过滤器或控制器来处理是否需要认证
|
||||
if (token == null) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从token中获取用户名
|
||||
String username = jwtUtils.getUsernameFromToken(token);
|
||||
// System.out.println("username: " + username);
|
||||
|
||||
// 如果用户名不为空且当前上下文没有认证信息,则进行认证
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,31 +44,21 @@ 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(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()
|
||||
// 配置会话管理,使用无状态会话策略
|
||||
@@ -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()
|
||||
|
||||
@@ -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,6 +66,7 @@ public class AuthController {
|
||||
|
||||
/**
|
||||
* 用户登录接口
|
||||
*
|
||||
* @param loginRequest 登录请求参数
|
||||
* @return 登录结果
|
||||
*/
|
||||
@@ -71,7 +76,8 @@ public class AuthController {
|
||||
|
||||
try {
|
||||
// 创建认证令牌
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||
loginRequest.getUsername(), loginRequest.getPassword());
|
||||
|
||||
// 执行认证
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
@@ -100,6 +106,7 @@ public class AuthController {
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*
|
||||
* @return 当前用户信息
|
||||
*/
|
||||
@PostMapping("/info")
|
||||
@@ -120,6 +127,7 @@ public class AuthController {
|
||||
|
||||
/**
|
||||
* 用户登出接口
|
||||
*
|
||||
* @return 登出结果
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@@ -127,4 +135,11 @@ public class AuthController {
|
||||
SecurityContextHolder.clearContext();
|
||||
return ResponseMessage.successEmpty("登出成功");
|
||||
}
|
||||
|
||||
@GetMapping("/debug")
|
||||
public Map<String, String> debug(HttpServletRequest request) {
|
||||
return Collections.singletonMap(
|
||||
"Authorization",
|
||||
request.getHeader("Authorization"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public class Nonsense {
|
||||
|
||||
@Column(name = "time")
|
||||
private Date time;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,6 +11,8 @@ public class CategoryAttributeDto {
|
||||
@NotBlank(message = "属性名称不能为空")
|
||||
private String attributename;
|
||||
|
||||
private Integer articlecount;
|
||||
|
||||
// Getters and Setters
|
||||
public Integer getAttributeid() {
|
||||
return attributeid;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Categoryattribute> children;
|
||||
private List<CategoryAttributeDto> children;
|
||||
|
||||
// 构造方法
|
||||
public CategoryTreeDto() {
|
||||
}
|
||||
// 全参构造方法
|
||||
public CategoryTreeDto(Integer id, String name, List<Categoryattribute> children) {
|
||||
public CategoryTreeDto(Integer id, String name, List<CategoryAttributeDto> 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<Categoryattribute> getChildren() {
|
||||
public List<CategoryAttributeDto> getChildren() {
|
||||
return children;
|
||||
}
|
||||
public void setChildren(List<Categoryattribute> children) {
|
||||
public void setChildren(List<CategoryAttributeDto> children) {
|
||||
this.children = children;
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,11 @@ public interface ArticleRepository extends JpaRepository<Article, Integer> {
|
||||
* 根据文章ID查询文章信息的方法
|
||||
* 使用JPQL(Java Persistence Query Language)进行查询
|
||||
*
|
||||
* @param id 文章的唯一标识符,作为查询条件
|
||||
* @param articleid 文章的唯一标识符,作为查询条件
|
||||
* @return 返回一个Optional<Article>对象,可能包含文章信息,也可能为空(如果未找到对应ID的文章)
|
||||
*/
|
||||
@Query("SELECT a FROM Article a WHERE a.articleid = :id")
|
||||
Optional<Article> findById(@Param("id") Integer id);
|
||||
@Query("SELECT a FROM Article a WHERE a.articleid = :articleid")
|
||||
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")
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Categoryattribute> categoryAttributes = 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;
|
||||
}
|
||||
}
|
||||
@@ -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<List<Message>> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归按时间排序子节点
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
5
src/main/resources/META-INF/spring-devtools.properties
Normal file
5
src/main/resources/META-INF/spring-devtools.properties
Normal 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
|
||||
@@ -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
|
||||
|
||||
# ====================================================================
|
||||
# 日志配置 - 开发用(详细日志便于调试)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user