1.1 修改用户控制器支持调用内容领域微服务
启用Feign
在用户领域微服务的Spring Boot启动类上添加@EnableFeignClients注解,并指定 Feign 客户端所在的包路径::
java
package com.waylau.rednote.usermicroservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.waylau.rednote")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.waylau.rednote.common.interfaces.client"})
public class RednoteApplication {
public static void main(String[] args) {
SpringApplication.run(RednoteApplication.class, args);
}
}
同时,为了能够确保所有相关模块的组件、服务和配置都能被Spring容器正确加载,将scanBasePackages设置为顶层包"com.waylau.rednote"。
配置Feign客户端实现熔断降级
在公共模块下新建src/main/java/com/waylau/rednote/common/interfaces/client/ContentServiceClient.java,使用fallback实现熔断降级:
java
package com.waylau.rednote.common.interfaces.client;
import com.waylau.rednote.common.dto.NotesWithUserDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
/**
* ContentServiceClient ContentService Feign Client
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/21
**/
@FeignClient(
name = "rednote-content-microservice",
fallback = ContentServiceClientFallback.class // 指定降级实现类
)
public interface ContentServiceClient {
@GetMapping("/note/user/{userId}")
ResponseEntity<NotesWithUserDto> getNotesWithUser(
@PathVariable Long userId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "12") int size);
在公共模块下新建src/main/java/com/waylau/rednote/common/interfaces/client/ContentServiceClientFallback.java:
java
package com.waylau.rednote.common.interfaces.client;
import com.waylau.rednote.common.dto.NotesWithUserDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
/**
* ContentServiceClientFallback ContentServiceClient Fallback
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/21
**/
// 降级实现类
@Component
public class ContentServiceClientFallback implements ContentServiceClient {
@Override
public ResponseEntity<NotesWithUserDto> getNotesWithUser(Long userId, int page, int size) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
}
创建NotesWithUserDto类
在公共模块下新建src/main/java/com/waylau/rednote/common/dto/NotesWithUserDto.java:
java
package com.waylau.rednote.common.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* NotesWithUserDto NotesWithUser DTO
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/29
**/
@Getter
@Setter
@AllArgsConstructor
public class NotesWithUserDto {
private UserDto user;
private List<NoteExploreDto> noteList;
private int currentPage;
private int totalPages;
}
由于NotesWithUserDto依赖了NoteExploreDto,因此内容领域微服务模块的NoteExploreDto.java文件也要移到公共模块src/main/java/com/waylau/rednote/common/dto/下。
已过去之后,由于NoteExploreDto静态方法toExploreDto依赖了Note,因此会报错:
java
public static NoteExploreDto toExploreDto(Note note, UserDto user) {
NoteExploreDto dto = new NoteExploreDto();
dto.noteId = note.getNoteId();
dto.title = note.getTitle();
dto.cover = note.getImages().get(0);
dto.username = user.getUsername();
dto.avatar = user.getAvatar();
dto.userId = user.getUserId();
dto.likeCount = note.getLikeCount();
dto.isLiked = note.isLikedByUser(user.getUserId());
return dto;
}
需要将上述方法进行重构,移动到NoteService中,代码如下:
java
/**
* 转为DTO
*
* @param note
* @param user
* @return
*/
NoteExploreDto toExploreDto(Note note, UserDto user);
NoteServiceImpl实现上述接口代码如下:
java
@Override
public NoteExploreDto toExploreDto(Note note, UserDto user) {
NoteExploreDto dto = new NoteExploreDto();
dto.setNoteId(note.getNoteId());
dto.setTitle(note.getTitle());
dto.setCover(note.getImages().get(0));
dto.setUsername(user.getUsername());
dto.setAvatar(user.getAvatar());
dto.setUserId(user.getUserId());
dto.setLikeCount(note.getLikeCount());
dto.setLiked(note.isLikedByUser(user.getUserId()));
return dto;
}
涉及上述接口的相关类也要调整,比如ExploreController:
java
@GetMapping("/note")
public ResponseEntity<NoteResponseDto> getNotesByCategory(
@RequestParam(defaultValue = "1") int page,
@RequestParam(required = false) String category,
@RequestParam(required = false) String query) {
// ...为节约篇幅,此处省略非核心内容
// 处理序列化问题
List<NoteExploreDto> noteExploreDtoList = new ArrayList<>();
for (Note note : notes.getContent()) {
/*noteExploreDtoList.add(NoteExploreDto.toExploreDto(note, currentUser));*/
noteExploreDtoList.add(noteService.toExploreDto(note, currentUser));
}
notesResponseDto.setNotes(noteExploreDtoList);
return ResponseEntity.ok(notesResponseDto);
}
在Controller调用Feign客户端
修改UserController,在原调用NoteService地方改为用ContentServiceClient:
java
@Autowired
//private NoteService noteService;
private ContentServiceClient contentServiceClient;
@GetMapping("/profile/{userId}")
public ResponseEntity<?> profileWithNotes(
@PathVariable Long userId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "12") int size) {
Optional<User> userOptional = userService.findByUserId(userId);
if (!userOptional.isPresent()) {
throw new UserNotFoundException("");
}
/*
User user = userOptional.get();
// 获取用户笔记列表(分页)
Page<Note> notePage = noteService.getNotesByUser(userId, page - 1, size);
// 转换为 DTO
List<NoteExploreDto> noteExploreDtoList = notePage.map(note -> NoteExploreDto.toExploreDto(note, user)).getContent();
Map<String, Object> map = new HashMap<>();
map.put("user", user);
map.put("noteList", noteExploreDtoList);
map.put("currentPage", page);
map.put("totalPages", notePage.getTotalPages());
return ResponseEntity.ok(map);
*/
ResponseEntity<NotesWithUserDto> notesWithUser = contentServiceClient.getNotesWithUser(userId, page, size);
NotesWithUserDto notesWithUserDto = notesWithUser.getBody();
return ResponseEntity.ok(notesWithUserDto);
}
在NoteController新增getNotesWithUser接口
java
@Autowired
//private UserService userService;
private UserServiceClient userServiceClient;
@GetMapping("/user/{userId}")
public ResponseEntity<?> getNotesWithUser(
@PathVariable Long userId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "12") int size) {
ResponseEntity<UserDto> response = userServiceClient.findByUserId(userId);
UserDto user = response.getBody();
// 获取用户笔记列表(分页)
Page<Note> notePage = noteService.getNotesByUser(userId, page - 1, size);
// 转换为 DTO
List<NoteExploreDto> noteExploreDtoList = notePage.map(note -> noteService.toExploreDto(note, user)).getContent();
NotesWithUserDto dto = new NotesWithUserDto(user, noteExploreDtoList, page, notePage.getTotalPages());
ResponseEntity<NotesWithUserDto> responseEntity = ResponseEntity.ok()
.body(dto);
return responseEntity;
}
新增用户领域微服务Feign客户端
在公共模块下新建src/main/java/com/waylau/rednote/common/interfaces/client/UserServiceClient.java,使用fallback实现熔断降级:
java
package com.waylau.rednote.common.interfaces.client;
import com.waylau.rednote.common.dto.*;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* UserServiceClient UserService Feign Client
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/21
**/
@FeignClient(
name = "rednote-user-microservice",
fallback = UserServiceClientFallback.class // 指定降级实现类
)
public interface UserServiceClient {
@GetMapping("/user/{userId}")
ResponseEntity<UserDto> findByUserId(@PathVariable Long userId);
}
在公共模块下新建src/main/java/com/waylau/rednote/common/interfaces/client/UserServiceClientFallback.java:
java
package com.waylau.rednote.common.interfaces.client;
import com.waylau.rednote.common.dto.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
/**
* UserServiceClientFallback UserServiceClient Fallback
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/21
**/
// 降级实现类
@Component
public class UserServiceClientFallback implements UserServiceClient {
@Override
public ResponseEntity<UserDto> findByUserId(Long userId) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
}
在UserController新增findByUserId接口
java
@GetMapping("/{userId}")
public ResponseEntity<UserDto> findByUserId(@PathVariable Long userId) {
Optional<User> userOptional = userService.findByUserId(userId);
if (!userOptional.isPresent()) {
throw new UserNotFoundException("");
}
User user = userOptional.get();
UserDto dto = new UserDto();
dto.setUserId(user.getUserId());
dto.setUsername(user.getUsername());
dto.setPhone(user.getPhone());
dto.setAvatar(user.getAvatar());
dto.setBio(user.getBio());
dto.setRole(user.getRole());
// 返回响应
return ResponseEntity.ok(dto);
}
在公共模块下新增src/main/java/com/waylau/rednote/common/dto/UserDto.java如下:
java
package com.waylau.rednote.common.dto;
import com.waylau.rednote.common.Role;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.*;
/**
* UserDto User DTO
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/21
**/
@Getter
@Setter
public class UserDto {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 手机号
*/
private String phone;
/**
* 头像
*/
private String avatar;
/**
* 简介
*/
private String bio;
/**
* 角色, 默认值设置为USER
*/
@Enumerated(EnumType.STRING)
private Role role = Role.USER;
}
1.2 修改用户控制器支持调用文件领域微服务
配置Feign客户端实现熔断降级
在公共模块下新建src/main/java/com/waylau/rednote/common/interfaces/client/FileServiceClient.java,使用fallback实现熔断降级:
java
package com.waylau.rednote.common.interfaces.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* FileServiceClient FileService Feign Client
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/21
**/
@FeignClient(
name = "rednote-file-microservice",
fallback = FileServiceClientFallback.class // 指定降级实现类
)
public interface FileServiceClient {
@PostMapping(value = "/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity<String> uploadImage(@RequestPart("file") MultipartFile file);
@DeleteMapping("/file/{fileId}")
ResponseEntity<String> deleteImage(@PathVariable String fileId);
}
在公共模块下新建src/main/java/com/waylau/rednote/common/interfaces/client/FileServiceClientFallback.java:
java
package com.waylau.rednote.common.interfaces.client;
import com.waylau.rednote.common.dto.ErrorResponseDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
/**
* FileServiceClientFallback FileServiceClient Fallback
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/21
**/
// 降级实现类
@Component
class FileServiceClientFallback implements FileServiceClient {
@Override
public ResponseEntity<String> uploadImage(MultipartFile file) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
@Override
public ResponseEntity<String> deleteImage(String fileId) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
}
在Controller调用Feign客户端
修改UserController,在原调用GridFSStorageService地方改为用FileServiceClient:
java
@Autowired
//private GridFSStorageService gridFSStorageService;
private FileServiceClient fileServiceClient;
@PostMapping("/edit")
public ResponseEntity<?> updateProfile(@RequestParam(required = true) String phone,
@RequestParam(required = false) String bio,
@RequestParam(required = false, value = "avatarFile") MultipartFile file) {
User currentUser = userService.getCurrentUser();
String oldAvatar = currentUser.getAvatar();
String fileUrl = null;
Map<String, String> map = new HashMap<>();
// 验证文件类型和大小
if (file !=null && !file.isEmpty()) {
// 验证文件类型
String contentType = file.getContentType();
if (!contentType.startsWith("image/")) {
map.put("error", "请上传图片文件");
return ResponseEntity.ok(map);
}
// 处理头像上传
/*
String fileId = gridFSStorageService.uploadImage(file);
fileUrl = MongoConfig.STATIC_PATH_PREFIX + fileId;
currentUser.setAvatar(fileUrl);
// 删除旧头像照片
if (oldAvatar !=null && !oldAvatar.isEmpty()) {
String oldFileId = oldAvatar.substring(MongoConfig.STATIC_PATH_PREFIX.length());
gridFSStorageService.deleteImage(oldFileId);
}*/
String fileId = fileServiceClient.uploadImage(file).getBody().toString();
fileUrl = FileConstant.STATIC_PATH_PREFIX + fileId;
currentUser.setAvatar(fileUrl);
// 删除旧头像照片
if (oldAvatar !=null && !oldAvatar.isEmpty()) {
String oldFileId = oldAvatar.substring(FileConstant.STATIC_PATH_PREFIX.length());
fileServiceClient.deleteImage(oldFileId);
}
}
// 更新用户信息
currentUser.setPhone(phone);
currentUser.setBio(bio);
userService.updateUser(currentUser);
map.put("success", "个人信息更新成功");
return ResponseEntity.ok(map);
}
修改常量
将MongoConfig的常量STATIC_PATH_PREFIX,移动到在公共模块下新类src/main/java/com/waylau/rednote/common/constant/FileConstant.java中:
java
package com.waylau.rednote.common.constant;
/**
* FileConstant 文件常量
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/21
**/
public class FileConstant {
public static String STATIC_PATH_PREFIX = "/file/";
}
1.3 修改用户领域微服务的认证服务
认证相关的等类
将rednote-common模块下的CustomAuthenticationFailureHandler、CustomAuthenticationProvider、CustomUserDetails、JwtAuthenticationFilter、JwtTokenProvider、UserDetailsServiceImpl、WebSecurityConfig等类迁移到rednote-user-microservice模块下。
迁移实体及仓库等类
将rednote-common模块下的User、UserRepository等类迁移到rednote-user-microservice模块下。
1.4 针对用户领域微服务的数据库配置
初始化数据库和表
创建名为"rednote_user_domain"新的数据库:
sql
CREATE DATABASE rednote_user_domain;
Seata 的 AT 模式需要通过 undo_log 表来实现分布式事务的回滚功能。因此,需要手动创建 undo_log 表。
sql
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
修改数据库配置
rednote改为rednote_user_domain
bash
spring.datasource.url=jdbc:mysql://localhost:3306/rednote_user_domain
应用启动之后,会初始化t_user等表。
sql
mysql> SHOW tables;
+-------------------------------+
| Tables_in_rednote_user_domain |
+-------------------------------+
| t_user |
| t_user_seq |
| undo_log |
+-------------------------------+
3 rows in set (0.019 sec)
同步数据
从rednote同步用户数据到rednote_user_domain:
sql
-- 清空表数据
TRUNCATE TABLE rednote_user_domain.t_user;
-- 前提:确保目标库 rednote_user_domain 已存在 t_user 表,且表结构与源表一致
INSERT INTO rednote_user_domain.t_user
(
user_id, username, password, role, bio, phone, avatar
)
SELECT
user_id, username, password, role, bio, phone, avatar
FROM rednote.t_user
ON DUPLICATE KEY UPDATE
-- 当主键/唯一键冲突时,更新指定字段(按需调整)
username = VALUES(username),
password = VALUES(password),
role = VALUES(role),
bio = VALUES(bio),
phone = VALUES(phone),
avatar = VALUES(avatar);
同步t_user_seq表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_user_domain.t_user_seq;
-- 前提:确保目标库 rednote_user_domain 已存在 t_user_seq 表,且表结构与源表一致
INSERT INTO rednote_user_domain.t_user_seq
(
next_val
)
SELECT
next_val
FROM rednote.t_user_seq;
删除多余的配置文件
将rednote-common模块下的resources目录删除。
移除Spring Boot DevTools
启动应用会报这个错:ClassNotFoundException: com.waylau.rednote.common.config.WebMvcConfig$$SpringCGLIB$$0
这个错误表明Seata在扫描业务Bean时试图加载一个不存在的CGLIB代理类。这是由于Seata与Spring Boot DevTools之间的兼容性问题导致的。
错误分析如下。
- 发生位置 :Seata的
GlobalTransactionScanner在扫描需要增强的业务Bean时 - 根本原因:Seata尝试加载由Spring CGLIB创建的代理类,但该类在类路径中不存在
解决方案排除DevTools重启类加载器影响。可以从父模块的pom.xml中移除DevTools依赖:
xml
<!-- 暂时移除这段依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
2.1 针对内容领域微服务的数据库配置
初始化数据库和表
创建名为"rednote_content_domain"新的数据库:
sql
CREATE DATABASE rednote_content_domain;
Seata 的 AT 模式需要通过 undo_log 表来实现分布式事务的回滚功能。因此,需要手动创建 undo_log 表。
sql
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
修改数据库配置
rednote改为rednote_content_domain
bash
spring.datasource.url=jdbc:mysql://localhost:3306/rednote_content_domain
修改实体
修改实体Note:
java
/*@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User author;*/
private Long userId;
// 判断当前用户是否已点赞
/*@Transient
public boolean isLikedByUser(Long userId) {
if (userId == null) {
return false;
}
return likes.stream().anyMatch(like -> like.getUser().getUserId().equals(userId));
}*/
@Transient
public boolean isLikedByUser(Long userId) {
if (userId == null) {
return false;
}
return likes.stream().anyMatch(like -> like.getUserId().equals(userId));
}
修改实体Like:
java
/*@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;*/
private Long userId;
修改实体Comment:
java
/*@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;*/
private Long userId;
修改NoteDetailDto
java
/*public static NoteDetailDto toNoteDetailDto(Note note, User user) {
NoteDetailDto dto = new NoteDetailDto();
dto.noteId = note.getNoteId();
dto.title = note.getTitle();
dto.content = note.getContent();
dto.images = note.getImages();
dto.category = note.getCategory();
dto.topics = note.getTopics();
dto.username = note.getAuthor().getUsername();
dto.avatar = note.getAuthor().getAvatar();
dto.userId = note.getAuthor().getUserId();
dto.likeCount = note.getLikeCount();
dto.isLiked = note.isLikedByUser(user.getUserId());
return dto;
}*/
public static NoteDetailDto toNoteDetailDto(Note note, UserDto user) {
NoteDetailDto dto = new NoteDetailDto();
dto.noteId = note.getNoteId();
dto.title = note.getTitle();
dto.content = note.getContent();
dto.images = note.getImages();
dto.category = note.getCategory();
dto.topics = note.getTopics();
dto.username = user.getUsername();
dto.avatar = user.getAvatar();
dto.userId = user.getUserId();
dto.likeCount = note.getLikeCount();
dto.isLiked = note.isLikedByUser(user.getUserId());
return dto;
}
修改仓库
修改NoteRepository
java
/**
* 根据作者的用户ID分页查询笔记
*
* @param userId
* @param pageable
* @return
*/
/*Page<Note> findByAuthorUserId(Long userId, Pageable pageable);*/
Page<Note> findByUserId(Long userId, Pageable pageable);
引用上述接口的地方一并更改,比如NoteServiceImpl。
修改LikeRepository
java
/*Optional<Like> findByUserUserIdAndNoteNoteId(Long userId, Long noteId);*/
Optional<Like> findByUserIdAndNoteNoteId(Long userId, Long noteId);
引用上述接口的地方一并更改,比如LikeServiceImpl。
2.2 修改笔记控制器支持调用用户领域微服务
启用Feign和Sentinel
在用户领域微服务的Spring Boot启动类上添加@EnableFeignClients注解,并指定 Feign 客户端所在的包路径::
java
package com.waylau.rednote.contentmicroservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.waylau.rednote")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.waylau.rednote.common.interfaces.client"})
public class RednoteApplication {
public static void main(String[] args) {
SpringApplication.run(RednoteApplication.class, args);
}
}
同时,为了能够确保所有相关模块的组件、服务和配置都能被Spring容器正确加载,将scanBasePackages设置为顶层包"com.waylau.rednote"。
用户领域微服务Feign客户端新增接口
在公共模块下src/main/java/com/waylau/rednote/common/interfaces/client/UserServiceClient.java下新增接口:
java
public interface UserServiceClient {
// ...为节约篇幅,此处省略非核心内容
@GetMapping("/user/current")
ResponseEntity<UserDto> getCurrentUser();
@GetMapping("/user/username/{username}")
ResponseEntity<UserDto> findByUsername(@PathVariable String username);
}
在公共模块下src/main/java/com/waylau/rednote/common/interfaces/client/UserServiceClientFallback.java下新增接口实现:
java
@Component
public class UserServiceClientFallback implements UserServiceClient {
// ...为节约篇幅,此处省略非核心内容
@Override
public ResponseEntity<UserDto> getCurrentUser() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
@Override
public ResponseEntity<UserDto> findByUsername(String username) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
}
在UserController新增接口
java
@GetMapping("/current")
public ResponseEntity<UserDto> getCurrentUser() {
User user = userService.getCurrentUser();
UserDto dto = new UserDto();
dto.setUserId(user.getUserId());
dto.setUsername(user.getUsername());
dto.setPhone(user.getPhone());
dto.setAvatar(user.getAvatar());
dto.setBio(user.getBio());
dto.setRole(user.getRole());
// 返回响应
return ResponseEntity.ok(dto);
}
@GetMapping("/username/{username}")
public ResponseEntity<?> findByUsername(@PathVariable String username) {
Optional<User> userOptional = userService.findByUsername(username);
if (!userOptional.isPresent()) {
throw new UserNotFoundException("");
}
User user = userOptional.get();
UserDto dto = new UserDto();
dto.setUserId(user.getUserId());
dto.setUsername(user.getUsername());
dto.setPhone(user.getPhone());
dto.setAvatar(user.getAvatar());
dto.setBio(user.getBio());
dto.setRole(user.getRole());
// 返回响应
return ResponseEntity.ok(dto);
}
在NoteController调用用户领域微服务Feign客户端
修改NoteController,在原调用UserService地方改为用UserServiceClient:
java
@PostMapping("/publish")
public ResponseEntity<?> publishNote(@Valid @ModelAttribute("note") NotePublishDto notePublishDto,
BindingResult bindingResult) {
// 验证表单
if (bindingResult.hasErrors()) {
// 自定义错误响应
Map<String, String> errors = new HashMap<>();
bindingResult.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
} else {
// 获取当前用户
/*User currentUser = userService.getCurrentUser();*/
UserDto currentUser = userServiceClient.getCurrentUser().getBody();
// 创建笔记
noteService.createNote(notePublishDto, currentUser);
// 返回成功响应
return ResponseEntity.ok("笔记创建成功");
}
}
@GetMapping("/{noteId}")
public ResponseEntity<?> getNote(@PathVariable Long noteId) {
Optional<Note> noteOptional = noteService.findByNoteId(noteId);
if (!noteOptional.isPresent()) {
throw new NoteNotFoundException("");
}
Note note = noteOptional.get();
/*User currentUser = userService.getCurrentUser();*/
UserDto author = userServiceClient.findByUserId(note.getUserId()).getBody();
return ResponseEntity.ok(NoteDetailDto.toNoteDetailDto(note, author));
}
2.3 修改笔记服务
修改createNote
修改NoteService的createNote接口:
java
Note createNote(NotePublishDto notePublishDto, UserDto author);
修改NoteServiceImpl的createNote方法:
java
@Autowired
//private GridFSStorageService gridFSStorageService;
private FileServiceClient fileServiceClient;
@Transactional
@Override
/*public Note createNote(NotePublishDto notePublishDto, User author) {
// 递增
redisTemplate.opsForValue().increment(NOTE_COUNT_KEY);
Note note = new Note();
// 字符串转为List
note.setTopics(StringUtil.splitToList(notePublishDto.getTopics()," "));
note.setTitle(notePublishDto.getTitle());
note.setContent(notePublishDto.getContent());
note.setCategory(notePublishDto.getCategory());
note.setAuthor(author);
// 处理图片上传
if (notePublishDto.getImages() != null) {
for (MultipartFile image : notePublishDto.getImages()) {
if (!image.isEmpty()) {
*//*String fileName = UUID.randomUUID() + "_" + image.getOriginalFilename();
String fileUrl = fileStorageService.saveFile(image, fileName);*//*
String fileId = gridFSStorageService.uploadImage(image);
String fileUrl = MongoConfig.STATIC_PATH_PREFIX + fileId;
note.getImages().add(fileUrl);
}
}
}
return noteRepository.save(note);
}*/
public Note createNote(NotePublishDto notePublishDto, UserDto author) {
// 递增
redisTemplate.opsForValue().increment(NOTE_COUNT_KEY);
Note note = new Note();
// 字符串转为List
note.setTopics(StringUtil.splitToList(notePublishDto.getTopics()," "));
note.setTitle(notePublishDto.getTitle());
note.setContent(notePublishDto.getContent());
note.setCategory(notePublishDto.getCategory());
// 用户对象改为了用户ID
note.setUserId(author.getUserId());
// 处理图片上传
if (notePublishDto.getImages() != null) {
for (MultipartFile image : notePublishDto.getImages()) {
if (!image.isEmpty()) {
// 调用文件领域微服务客户端
String fileId = fileServiceClient.uploadImage(image).getBody().toString();
String fileUrl = FileConstant.STATIC_PATH_PREFIX + fileId;
note.getImages().add(fileUrl);
}
}
}
return noteRepository.save(note);
}
修改deleteNote
修改NoteServiceImpl的deleteNote方法:
java
@Override
@Transactional
/*public void deleteNote(Note note) {
// 递增
redisTemplate.opsForValue().decrement(NOTE_COUNT_KEY);
// 注意:先删库再删文件。以防止删除文件异常时,方便回滚数据库
// 删除笔记
noteRepository.delete(note);
// 删除所有关联图片
for (String imageUri : note.getImages()) {
// 删除旧头像照片
*//*fileStorageService.deleteFile(imageUri);*//*
String fileId = imageUri.substring(MongoConfig.STATIC_PATH_PREFIX.length());
gridFSStorageService.deleteImage(fileId);
}
}*/
public void deleteNote(Note note) {
// 递增
redisTemplate.opsForValue().decrement(NOTE_COUNT_KEY);
// 注意:先删库再删文件。以防止删除文件异常时,方便回滚数据库
// 删除笔记
noteRepository.delete(note);
// 删除所有关联图片
for (String imageUri : note.getImages()) {
// 删除旧头像照片
/*fileStorageService.deleteFile(imageUri);*/
String fileId = imageUri.substring(FileConstant.STATIC_PATH_PREFIX.length());
fileServiceClient.deleteImage(fileId);
}
}
修改isAuthor
修改NoteServiceImpl的isAuthor方法:
java
@Autowired
private UserServiceClient userServiceClient;
@Override
/*public boolean isAuthor(Long noteId, String username) {
Note note = noteRepository.findByNoteId(noteId)
.orElseThrow(() -> new NoteNotFoundException(""));
return note.getAuthor().getUsername().equals(username);
}*/
public boolean isAuthor(Long noteId, String username) {
Note note = noteRepository.findByNoteId(noteId)
.orElseThrow(() -> new NoteNotFoundException(""));
UserDto userDto = userServiceClient.findByUsername(username).getBody();
return note.getUserId().equals(userDto.getUserId());
}
修改toExploreDto
修改NoteServiceImpl的toExploreDto方法:
java
@Override
public NoteExploreDto toExploreDto(Note note, UserDto user) {
NoteExploreDto noteExploreDto = new NoteExploreDto();
noteExploreDto.setNoteId(note.getNoteId());
noteExploreDto.setTitle(note.getTitle());
noteExploreDto.setCover(note.getImages().get(0));
/*noteExploreDto.setUsername(note.getAuthor().getUsername());
noteExploreDto.setAvatar(note.getAuthor().getAvatar());
noteExploreDto.setUserId(note.getAuthor().getUserId());*/
noteExploreDto.setUsername(user.getUsername());
noteExploreDto.setAvatar(user.getAvatar());
noteExploreDto.setUserId(user.getUserId());
// 赋值isLiked、likeCount属性
noteExploreDto.setLiked(note.isLikedByUser(user.getUserId()));
noteExploreDto.setLikeCount(note.getLikeCount());
return noteExploreDto;
}
2.4 修改点赞控制器支持调用用户领域微服务
在LikeController调用用户领域微服务Feign客户端
修改LikeController,在原调用UserService地方改为用UserServiceClient:
java
@Autowired
//private UserService userService;
private UserServiceClient userServiceClient;
@PostMapping("/{noteId}")
public ResponseEntity<?> toggleLike(@PathVariable Long noteId) {
/*User currentUser = userService.getCurrentUser();*/
UserDto currentUser = userServiceClient.getCurrentUser().getBody();
boolean isLiked = likeService.toggleLike(noteId, currentUser);
long likeCount = likeService.getLikeCount(noteId);
return ResponseEntity.ok(new LikeResponseDto(isLiked, likeCount));
}
修改点赞服务
修改LikeService的toggleLike接口:
java
/**
* 点赞/取消点赞
*
* @param noteId
* @param user
* @return
*/
/*boolean toggleLike(Long noteId, User user);*/
boolean toggleLike(Long noteId, UserDto user);
修改LikeServiceImpl的toggleLike方法:
java
@Override
@Transactional
public boolean toggleLike(Long noteId, UserDto user) {
// ...为节约篇幅,此处省略非核心内容
} else {
// ...为节约篇幅,此处省略非核心内容
// 数据库增加数据
Like like = new Like();
// 将用户对象改为了用户ID
/*like.setUser(user);*/
like.setUserId(user.getUserId());
like.setNote(note);
likeRepository.save(like);
return true;
}
}
2.5 修改评论控制器支持调用用户领域微服务
在CommentController调用用户领域微服务Feign客户端
修改CommentController,在原调用UserService地方改为用UserServiceClient:
java
@PostMapping("/{noteId}")
public ResponseEntity<?> createComment(
@PathVariable Long noteId,
@RequestBody String content) {
// ...为节约篇幅,此处省略非核心内容
/*User currentUser = userService.getCurrentUser();*/
UserDto currentUser = userServiceClient.getCurrentUser().getBody();
// ...为节约篇幅,此处省略非核心内容
}
@PostMapping("/{noteId}/reply/{parentId}")
public ResponseEntity<?> replyToComment(
@PathVariable Long noteId,
@PathVariable Long parentId,
@RequestBody String content) {
// ...为节约篇幅,此处省略非核心内容
/*User currentUser = userService.getCurrentUser();*/
UserDto currentUser = userServiceClient.getCurrentUser().getBody();
// ...为节约篇幅,此处省略非核心内容
}
@DeleteMapping("/{commentId}")
public ResponseEntity<?> deleteComment(
@PathVariable Long commentId) {
// ...为节约篇幅,此处省略非核心内容
/*User currentUser = userService.getCurrentUser();*/
UserDto currentUser = userServiceClient.getCurrentUser().getBody();
// 验证父评论属于指定笔记
// 将用户对象改为了用户ID
/*if (!comment.getUser().getUserId().equals(currentUser.getUserId())) {
throw new CommentNotFoundException("无权删除此评论");
}*/
if (!comment.getUserId().equals(currentUser.getUserId())) {
throw new CommentNotFoundException("无权删除此评论");
}
// ...为节约篇幅,此处省略非核心内容
}
修改评论服务
修改createComment
修改CommentService的createComment接口:
java
/**
* 创建评论
*
* @param note
* @param user
* @param content
* @return
*/
/*Comment createComment(Note note, User user, String content);*/
Comment createComment(Note note, UserDto user, String content);
修改CommentServiceImpl的createComment方法:
java
@Override
@Transactional
/*public Comment createComment(Note note, User user, String content) {
// 递增
redisTemplate.opsForValue().increment(COMMENT_COUNT_KEY);
Comment comment = new Comment();
comment.setContent(content);
comment.setUser(user);
comment.setNote(note);
return commentRepository.save(comment);
}*/
public Comment createComment(Note note, UserDto user, String content) {
// 递增
redisTemplate.opsForValue().increment(COMMENT_COUNT_KEY);
Comment comment = new Comment();
comment.setContent(content);
// 将用户对象改为用户ID
comment.setUserId(user.getUserId());
comment.setNote(note);
return commentRepository.save(comment);
}
修改replyToComment
修改CommentService的replyToComment接口:
java
/**
* 回复评论
*
* @param note
* @param parentComment
* @param user
* @param content
* @return
*/
/*Comment replyToComment(Note note, Comment parentComment, User user, String content);*/
Comment replyToComment(Note note, Comment parentComment, UserDto user, String content);
修改CommentServiceImpl的replyToComment方法:
java
@Override
@Transactional
/*public Comment replyToComment(Note note, Comment parentComment, User user, String content) {
// 递增
redisTemplate.opsForValue().increment(COMMENT_COUNT_KEY);
Comment reply = new Comment();
reply.setContent(content);
reply.setUser(user);
reply.setNote(note);
reply.setParent(parentComment);
parentComment.getReplies().add(reply);
return commentRepository.save(reply);
}*/
public Comment replyToComment(Note note, Comment parentComment, UserDto user, String content) {
// 递增
redisTemplate.opsForValue().increment(COMMENT_COUNT_KEY);
Comment reply = new Comment();
reply.setContent(content);
// 将用户对象改为用户ID
reply.setUserId(user.getUserId());
reply.setNote(note);
reply.setParent(parentComment);
parentComment.getReplies().add(reply);
return commentRepository.save(reply);
}
2.6 修改首页笔记探索控制器支持调用用户领域微服务
在ExploreController调用用户领域微服务Feign客户端
修改ExploreController,在原调用UserService地方改为用UserServiceClient:
java
@GetMapping("/note")
public ResponseEntity<NoteResponseDto> getNotesByCategory(
@RequestParam(defaultValue = "1") int page,
@RequestParam(required = false) String category,
@RequestParam(required = false) String query) {
// ...为节约篇幅,此处省略非核心内容
/*User currentUser = userService.getCurrentUser();*/
List<NoteExploreDto> noteExploreDtoList = new ArrayList<>();
for (Note note : notes.getContent()) {
/*noteExploreDtoList.add(NoteExploreDto.toExploreDto(note, currentUser));*/
UserDto author = userServiceClient.findByUserId(note.getUserId()).getBody();
noteExploreDtoList.add(noteService.toExploreDto(note, author));
}
notesResponseDto.setNotes(noteExploreDtoList);
ResponseEntity<NoteResponseDto> responseEntity = ResponseEntity.ok(notesResponseDto);
return responseEntity;
}
2.7 重构CommentResponseDto中的toCommentResponseDto
CommentResponseDto中的toCommentResponseDto需要重构:
java
public static CommentResponseDto toCommentResponseDto(Comment comment) {
if (comment == null) {
return null;
}
CommentResponseDto dto = new CommentResponseDto();
dto.commentId = comment.getCommentId();
dto.content = comment.getContent();
dto.noteId = comment.getNote().getNoteId();
dto.createdAt = comment.getCreatedAt();
Comment parentComment = comment.getParent();
if (parentComment != null) {
dto.parentCommentId = parentComment.getCommentId();
dto.parentCommentUsername = parentComment.getUser().getUsername();
}
List<Comment> replyList = comment.getReplies();
if (replyList != null && !replyList.isEmpty()) {
dto.replies = replyList.stream().map(c -> CommentResponseDto.toCommentResponseDto(c))
.collect(Collectors.toList());
}
User user = comment.getUser();
dto.username = user.getUsername();
dto.avatar = user.getAvatar();
dto.userId = user.getUserId();
return dto;
}
因为依赖了User实体,改为服务的方法。
改造Comment实体
Comment实体冗余多一个字段username,以便将用户的名称保存下来,避免调用用户领域微服务来查询:
java
private String username;
CommentServiceImpl修改:
java
public Comment createComment(Note note, UserDto user, String content) {
// ...为节约篇幅,此处省略非核心内容
// 冗余username
comment.setUsername(user.getUsername());
return commentRepository.save(comment);
}
public Comment replyToComment(Note note, Comment parentComment, UserDto user, String content) {
// ...为节约篇幅,此处省略非核心内容
// 冗余username
reply.setUsername(user.getUsername());
parentComment.getReplies().add(reply);
return commentRepository.save(reply);
}
将toCommentResponseDto移到到CommentService并调用用户领域微服务
CommentService新增如下接口:
java
/**
* 转为DTO
* @param comment
* @return
*/
CommentResponseDto toCommentResponseDto(Comment comment);
CommentServiceImpl新增如下方法:
java
@Override
public CommentResponseDto toCommentResponseDto(Comment comment) {
if (comment == null) {
return null;
}
CommentResponseDto dto = new CommentResponseDto();
dto.setCommentId(comment.getCommentId());
dto.setContent(comment.getContent());
dto.setNoteId(comment.getNote().getNoteId());
dto.setCreatedAt(comment.getCreatedAt());
Comment parentComment = comment.getParent();
if (parentComment != null) {
dto.setParentCommentId(parentComment.getCommentId());
dto.setParentCommentUsername(parentComment.getUsername());
}
List<Comment> replyList = comment.getReplies();
if (replyList != null && !replyList.isEmpty()) {
dto.setReplies(replyList.stream().map(c -> toCommentResponseDto(c))
.collect(Collectors.toList()));
}
UserDto userDto = (UserDto)userServiceClient.findByUserId(comment.getUserId()).getBody();
dto.setUsername(userDto.getUsername());
dto.setAvatar(userDto.getAvatar());
dto.setUserId(userDto.getUserId());
return dto;
}
修改评论控制器
将原先调用CommentResponseDto.toCommentResponseDto的地方全部改为commentService.toCommentResponseDto:
java
@PostMapping("/{noteId}")
public ResponseEntity<?> createComment(
@PathVariable Long noteId,
@RequestBody String content) {
// ...为节约篇幅,此处省略非核心内容
// 转为DTO
/*CommentResponseDto commentResponseDto = CommentResponseDto.toCommentResponseDto(comment);*/
CommentResponseDto commentResponseDto = commentService.toCommentResponseDto(comment);
return ResponseEntity.ok(commentResponseDto);
}
@GetMapping("/{noteId}")
public ResponseEntity<?> getComments(@PathVariable Long noteId) {
// ...为节约篇幅,此处省略非核心内容
// 转为DTO
/*List<CommentResponseDto> commentResponseDtoList = comments.stream().map(c -> CommentResponseDto.toCommentResponseDto(c))
.collect(Collectors.toList());*/
List<CommentResponseDto> commentResponseDtoList = comments.stream().map(c -> commentService.toCommentResponseDto(c))
.collect(Collectors.toList());
return ResponseEntity.ok(commentResponseDtoList);
}
@PostMapping("/{noteId}/reply/{parentId}")
public ResponseEntity<?> replyToComment(
@PathVariable Long noteId,
@PathVariable Long parentId,
@RequestBody String content) {
// ...为节约篇幅,此处省略非核心内容
// 转为DTO
/*CommentResponseDto commentResponseDto = CommentResponseDto.toCommentResponseDto(reply);*/
CommentResponseDto commentResponseDto = commentService.toCommentResponseDto(reply);
return ResponseEntity.ok(commentResponseDto);
}
2.8 内容领域微服务数据同步
应用启动之后,会初始化t_note等表。
perl
mysql> SHOW tables;
+----------------------------------+
| Tables_in_rednote_content_domain |
+----------------------------------+
| note_images |
| note_topics |
| t_comment |
| t_comment_seq |
| t_like |
| t_like_seq |
| t_note |
| t_note_seq |
| undo_log |
+----------------------------------+
9 rows in set (0.030 sec)
同步数据
从rednote同步用户数据到rednote_content_domain,主要涉及t_note、t_like、t_comment、note_topics、note_images、t_note_seq、t_like_seq、t_comment_seq等表。
同步t_note表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_content_domain.t_note;
-- 前提:确保目标库 rednote_content_domain 已存在 t_note 表,且表结构与源表一致
INSERT INTO rednote_content_domain.t_note
(
note_id, user_id, title, category, content, create_at, update_at
)
SELECT
note_id, user_id, title, category, content, create_at, update_at
FROM rednote.t_note;
同步t_like表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_content_domain.t_like;
-- 前提:确保目标库 rednote_content_domain 已存在 t_like 表,且表结构与源表一致
INSERT INTO rednote_content_domain.t_like
(
note_id, user_id, like_id, create_at
)
SELECT
note_id, user_id, like_id, create_at
FROM rednote.t_like;
同步t_comment表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_content_domain.t_comment;
-- 前提:确保目标库 rednote_content_domain 已存在 t_comment 表,且表结构与源表一致
INSERT INTO rednote_content_domain.t_comment
(
note_id, user_id, comment_id, content, create_at, parent_id
)
SELECT
note_id, user_id, comment_id, content, create_at, parent_id
FROM rednote.t_comment;
同步note_topics表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_content_domain.note_topics;
-- 前提:确保目标库 rednote_content_domain 已存在 note_topics 表,且表结构与源表一致
INSERT INTO rednote_content_domain.note_topics
(
note_note_id, topics
)
SELECT
note_note_id, topics
FROM rednote.note_topics;
同步note_images表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_content_domain.note_images;
-- 前提:确保目标库 rednote_content_domain 已存在 note_images 表,且表结构与源表一致
INSERT INTO rednote_content_domain.note_images
(
note_note_id, images
)
SELECT
note_note_id, images
FROM rednote.note_images;
同步t_note_seq表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_content_domain.t_note_seq;
-- 前提:确保目标库 rednote_content_domain 已存在 t_note_seq 表,且表结构与源表一致
INSERT INTO rednote_content_domain.t_note_seq
(
next_val
)
SELECT
next_val
FROM rednote.t_note_seq;
同步t_like_seq表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_content_domain.t_like_seq;
-- 前提:确保目标库 rednote_content_domain 已存在 t_like_seq 表,且表结构与源表一致
INSERT INTO rednote_content_domain.t_like_seq
(
next_val
)
SELECT
next_val
FROM rednote.t_like_seq;
同步t_comment_seq表:
sql
-- 清空表数据
TRUNCATE TABLE rednote_content_domain.t_comment_seq;
-- 前提:确保目标库 rednote_content_domain 已存在 t_comment_seq 表,且表结构与源表一致
INSERT INTO rednote_content_domain.t_comment_seq
(
next_val
)
SELECT
next_val
FROM rednote.t_comment_seq;
3.1 修改后台管理界面控制器支持调用用户领域微服务
启用Feign和Sentinel
在用户领域微服务的Spring Boot启动类上添加@EnableFeignClients注解,并指定 Feign 客户端所在的包路径::
java
package com.waylau.rednote.adminmicroservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.waylau.rednote")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.waylau.rednote.common.interfaces.client"})
public class RednoteApplication {
public static void main(String[] args) {
SpringApplication.run(RednoteApplication.class, args);
}
}
同时,为了能够确保所有相关模块的组件、服务和配置都能被Spring容器正确加载,将scanBasePackages设置为顶层包"com.waylau.rednote"。
用户领域微服务Feign客户端新增接口
在公共模块下src/main/java/com/waylau/rednote/common/interfaces/client/UserServiceClient.java下新增接口:
java
public interface UserServiceClient {
// ...为节约篇幅,此处省略非核心内容
@GetMapping("/user")
ResponseEntity<AllUsersDto> getAllUsers(@RequestParam(defaultValue = "1") int page);
@PostMapping("/user/edit-by-admin")
ResponseEntity<String> updateUserByAdmin(@RequestBody UserEditByAdminDto user);
@DeleteMapping("/user/{userId}")
ResponseEntity<DeleteResponseDto> deleteUser(@PathVariable Long userId);
@GetMapping("/user/count")
ResponseEntity<Long> countUsers();
}
新增src/main/java/com/waylau/rednote/common/dto/AllUsersDto.java如下:
java
package com.waylau.rednote.common.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* AllUsersDto
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/29
**/
@Getter
@Setter
@AllArgsConstructor
public class AllUsersDto {
private List<UserEditByAdminDto> userList;
private int currentPage;
private int totalPages;
}
新增src/main/java/com/waylau/rednote/common/dto/UserEditByAdminDto.java如下:
java
package com.waylau.rednote.common.dto;
import com.waylau.rednote.common.Role;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.Getter;
import lombok.Setter;
/**
* UserEditByAdminDto
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/24
**/
@Getter
@Setter
public class UserEditByAdminDto {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 手机号
*/
private String phone;
/**
* 密码
*/
private String password;
/**
* 角色, 默认值设置为USER
*/
@Enumerated(EnumType.STRING)
private Role role = Role.USER;
}
在公共模块下src/main/java/com/waylau/rednote/common/interfaces/client/UserServiceClientFallback.java下新增接口实现:
java
@Component
public class UserServiceClientFallback implements UserServiceClient {
// ...为节约篇幅,此处省略非核心内容
@Override
public ResponseEntity<AllUsersDto> getAllUsers(int page) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
@Override
public ResponseEntity<String> updateUserByAdmin(UserEditByAdminDto user) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
@Override
public ResponseEntity<DeleteResponseDto> deleteUser(Long userId) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
@Override
public ResponseEntity<Long> countUsers() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
}
在UserController新增接口
java
private static final int PAGE_SIZE = 10;
@GetMapping
public ResponseEntity<AllUsersDto> getAllUsers(@RequestParam(defaultValue = "1") int page) {
Page<User> userPage = userService.getAllUsers(page, PAGE_SIZE);
// 转换为 DTO
List<UserEditByAdminDto> userDtoList = userPage.map(user -> {
UserEditByAdminDto dto = new UserEditByAdminDto();
dto.setUserId(user.getUserId());
dto.setUsername(user.getUsername());
dto.setPhone(user.getPhone());
dto.setRole(user.getRole());
return dto;
}).getContent();
AllUsersDto allUsersDto = new AllUsersDto(userDtoList, page, userPage.getTotalPages());
return ResponseEntity.ok(allUsersDto);
}
@PostMapping("/edit-by-admin")
public ResponseEntity<String> updateUserByAdmin(@RequestBody UserEditByAdminDto user) {
Optional<User> userOptional = userService.findByUserId(user.getUserId());
if (!userOptional.isPresent()) {
throw new UserNotFoundException("");
}
User oldUser = userOptional.get();
userService.updateUserByAdmin(oldUser, user);
return ResponseEntity.ok("更新成功");
}
@DeleteMapping("/{userId}")
public ResponseEntity<DeleteResponseDto> deleteUser(@PathVariable Long userId) {
Optional<User> userOptional = userService.findByUserId(userId);
if (!userOptional.isPresent()) {
throw new UserNotFoundException("");
}
userService.deleteUser(userId);
DeleteResponseDto response = new DeleteResponseDto();
response.setMessage("用户删除成功");
response.setRedirectUrl("/admin/user");
return ResponseEntity.ok(response);
}
@GetMapping("/count")
public ResponseEntity<Long> countUsers() {
long userCount = userService.countUsers();
return ResponseEntity.ok(userCount);
}
修改用户服务
修改UserService的updateUserByAdmin接口:
java
/**
* 管理员更新用户
*
* @param oldUser
* @param newUser
*/
/*void updateUserByAdmin(User oldUser, User newUser);*/
void updateUserByAdmin(User oldUser, UserEditByAdminDto newUser);
修改UserServiceImpl的updateUserByAdmin方法:
java
@Override
/*public void updateUserByAdmin(User oldUser, User newUser) {
// 更新基本信息
oldUser.setPhone(newUser.getPhone());
// 更新密码(如果有输入)
if (newUser.getPassword() !=null && !newUser.getPassword().isEmpty()) {
oldUser.setPassword(passwordEncoder.encode(newUser.getPassword()));
}
userRepository.save(oldUser);
}*/
public void updateUserByAdmin(User oldUser, UserEditByAdminDto newUser) {
// 更新基本信息
oldUser.setPhone(newUser.getPhone());
// 更新密码(如果有输入)
if (newUser.getPassword() !=null && !newUser.getPassword().isEmpty()) {
oldUser.setPassword(passwordEncoder.encode(newUser.getPassword()));
}
userRepository.save(oldUser);
}
在AdminController调用用户领域微服务Feign客户端
修改AdminController,在原调用UserService地方改为用UserServiceClient:
java
@GetMapping("/user")
/*public ResponseEntity<?> getUser(@RequestParam(defaultValue = "1") int page) {
Page<User> userPage = userService.getAllUsers(page, PAGE_SIZE);
Map<String, Object> map = new HashMap<>();
map.put("userList", userPage.getContent());
map.put("currentPage", page);
map.put("totalPages", userPage.getTotalPages());
return ResponseEntity.ok(map);
}*/
public ResponseEntity<?> getUser(@RequestParam(defaultValue = "1") int page) {
ResponseEntity<AllUsersDto> responseEntity = userServiceClient.getAllUsers(page);
return ResponseEntity.ok(responseEntity.getBody());
}
@GetMapping("/user/{userId}/edit")
/*public ResponseEntity<?> editUser(@PathVariable Long userId) {
Optional<User> userOptional = userService.findByUserId(userId);
if (!userOptional.isPresent()) {
throw new UserNotFoundException("");
}
return ResponseEntity.ok(userOptional.get());
}*/
public ResponseEntity<UserDto> editUser(@PathVariable Long userId) {
ResponseEntity<UserDto> responseEntity = userServiceClient.findByUserId(userId);
return ResponseEntity.ok(responseEntity.getBody());
}
@PostMapping("/user")
/*public ResponseEntity<?> updateUser(@ModelAttribute User user) {
Optional<User> userOptional = userService.findByUserId(user.getUserId());
if (!userOptional.isPresent()) {
throw new UserNotFoundException("");
}
User oldUser = userOptional.get();
userService.updateUserByAdmin(oldUser, user);
return ResponseEntity.ok("更新成功");
}*/
public ResponseEntity<?> updateUser(@ModelAttribute UserEditByAdminDto user) {
ResponseEntity<?> responseEntity = userServiceClient.updateUserByAdmin(user);
return ResponseEntity.ok(responseEntity.getBody());
}
@DeleteMapping("/user/{userId}")
/*public ResponseEntity<?> deleteUser(@PathVariable Long userId) {
Optional<User> userOptional = userService.findByUserId(userId);
if (!userOptional.isPresent()) {
throw new UserNotFoundException("");
}
userService.deleteUser(userId);
DeleteResponseDto response = new DeleteResponseDto();
response.setMessage("用户删除成功");
response.setRedirectUrl("/admin/user");
return ResponseEntity.ok(response);
}*/
public ResponseEntity<?> deleteUser(@PathVariable Long userId) {
ResponseEntity<DeleteResponseDto> responseEntity = userServiceClient.deleteUser(userId);
return ResponseEntity.ok(responseEntity.getBody());
}
3.2 修改后台管理界面控制器支持调用内容领域微服务
内容领域微服务Feign客户端新增接口
在公共模块下src/main/java/com/waylau/rednote/common/interfaces/client/ContentServiceClient.java下新增接口:
java
public interface ContentServiceClient {
// ...为节约篇幅,此处省略非核心内容
@GetMapping("/log/dashboard")
ResponseEntity<AdminDashboardDto> dashboard();
}
新增src/main/java/com/waylau/rednote/common/dto/AdminDashboardDto.java如下:
java
package com.waylau.rednote.common.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* NotesWithUserDto NotesWithUser DTO
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/29
**/
@Getter
@Setter
@AllArgsConstructor
public class AdminDashboardDto {
private long userCount;
private long noteCount;
private long commentCount;
private List<NoteBrowseCountDto> noteBrowseCountDtoList;
private List<NoteBrowseTimeDto> noteBrowseTimeDtoList;
}
AdminDashboardDto所依赖的NoteBrowseCountDto、NoteBrowseTimeDto一并移到公共模块下。
在公共模块下src/main/java/com/waylau/rednote/common/interfaces/client/ContentServiceClientFallback.java下新增接口实现:
java
@Component
public class ContentServiceClientFallback implements ContentServiceClient {
// ...为节约篇幅,此处省略非核心内容
@Override
public ResponseEntity<AdminDashboardDto> dashboard() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
}
调用Feign客户端
AdminController调用Feign客户端:
java
@GetMapping("/dashboard")
/*public ResponseEntity<?> dashboard() {
// 统计数据
long userCount = userService.countUsers();
long noteCount = noteService.countNotes();
long commentCount = commentService.countComments();
List<NoteBrowseCountDto> noteBrowseCountDtoList = noteService.getNoteByBrowseCount(1, 10);
List<NoteBrowseTimeDto> noteBrowseTimeDtoList = noteService.getNoteByBrowseTime(1, 10);
Map<String, Object> map = new HashMap<>();
map.put("userCount", userCount);
map.put("noteCount", noteCount);
map.put("commentCount", commentCount);
map.put("noteBrowseCountDtoList", noteBrowseCountDtoList);
map.put("noteBrowseTimeDtoList", noteBrowseTimeDtoList);
return ResponseEntity.ok(map);
}*/
public ResponseEntity<?> dashboard() {
ResponseEntity<AdminDashboardDto> responseEntity = contentServiceClient.dashboard();
return ResponseEntity.ok(responseEntity.getBody());
}
在LogController新增接口
java
@Autowired
private UserServiceClient userServiceClient;
@Autowired
private NoteService noteService;
@Autowired
private CommentService commentService;
@GetMapping("/dashboard")
public ResponseEntity<AdminDashboardDto> dashboard() {
// 统计数据
long userCount = userServiceClient.countUsers().getBody();
long noteCount = noteService.countNotes();
long commentCount = commentService.countComments();
List<NoteBrowseCountDto> noteBrowseCountDtoList = noteService.getNoteByBrowseCount(1, 10);
List<NoteBrowseTimeDto> noteBrowseTimeDtoList = noteService.getNoteByBrowseTime(1, 10);
AdminDashboardDto adminDashboardDto =
new AdminDashboardDto(userCount, noteCount, commentCount, noteBrowseCountDtoList, noteBrowseTimeDtoList);
return ResponseEntity.ok(adminDashboardDto);
}
3.3 启用全局事务管理
在以下方法上增加@GlobalTransactional事务。
NoteServiceImpl:
java
/*@Transactional*/
@Override
@GlobalTransactional
public Note createNote(NotePublishDto notePublishDto, UserDto author) {
// ...为节约篇幅,此处省略非核心内容
}
/*@Transactional*/
@Override
@GlobalTransactional
public void deleteNote(Note note) {
// ...为节约篇幅,此处省略非核心内容
}
UserController:
java
@PostMapping("/edit")
@GlobalTransactional
public ResponseEntity<?> updateProfile(@RequestParam(required = true) String phone,
@RequestParam(required = false) String bio,
@RequestParam(required = false, value = "avatarFile") MultipartFile file) {
// ...为节约篇幅,此处省略非核心内容
}
对于标记了 @GlobalTransactional 的方法(协调远程调用和本地操作),可以省略 @Transactional,因为:
@GlobalTransactional已隐含了本地事务的管理能力(默认会创建本地事务)。- 叠加使用不会报错,但属于冗余配置(
@GlobalTransactional优先级更高)。
3.4 采用令牌传递机制处理微服务之间的调用权限
在微服务架构中,OpenFeign客户端之间的调用权限处理需要兼顾安全性和易用性,通常采用"令牌传递"或"服务间认证"机制。
为什么认证过滤器中不能查库?
- 性能损耗:每个请求都查库会导致数据库压力增大,尤其在高并发场景下。
- 服务耦合:所有微服务都需要连接用户数据库,违背微服务"数据隔离"原则。
- 可用性风险:若用户服务数据库故障,所有依赖它的服务都会受影响。
核心原则:JWT 自包含用户信息
JWT 的设计理念是自包含令牌,即令牌本身已包含足够的用户信息(如用户名、ID、权限),无需每次请求都查询数据库。因此:
- 查库逻辑仅需在用户登录时执行一次,用于验证用户身份并生成包含必要信息的 JWT。
- 后续所有请求通过解析 JWT 获取用户信息,不再查库。
因此在微服务架构中,需要移除 JwtAuthenticationFilter 中的 UserDetailsService 依赖后,用户信息的查询逻辑应放在认证阶段(登录时),而不是在每次请求的过滤器中。
查库逻辑的正确位置:登录接口(认证服务)
以用户服务为例,登录接口是查询用户信息的唯一时机,具体流程修改AuthController如下:
java
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<?> processLoginForm(@Valid @RequestBody UserLoginDto loginDto,
BindingResult bindingResult) {
// ...为节约篇幅,此处省略非核心内容
/*
// 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginDto.getUsername(),
loginDto.getPassword()
)
);
// 生成JWT
String jwt = tokenProvider.generateToken(authentication);*/
// 生成JWT
// 1. 获取用户信息(从数据库获取)
CustomUserDetails userDetails = (CustomUserDetails)userDetailsService.loadUserByUsername(loginDto.getUsername());
// 2. 生成 JWT(包含用户ID、用户名、权限等信息)
String jwt = tokenProvider.generateToken(userDetails);
// 返回响应
return ResponseEntity.ok(jwt);
}
关键:JWT 生成时已包含用户的核心信息(ID、权限等),后续请求无需再查库,直接解析 JWT 即可。
修改认证过滤器
修改JwtAuthenticationFilter如下:
java
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
try {
// 从请求头中获取JWT
String jwt = getJwtFromRequest(request);
if (jwt != null && tokenProvider.validateJwtToken(jwt)) {
// 从JWT中获取用户名
String username = tokenProvider.getUsernameFromJwtToken(jwt);
// 加载用户信息
/*UserDetails userDetails = userDetailsService.loadUserByUsername(username);*/
// 1. 从JWT中获取用户信息
Long userId = tokenProvider.getUserIdFromJwtToken(jwt); // 新增:解析用户ID
Collection<? extends GrantedAuthority> authorities = tokenProvider.getAuthoritiesFromJwtToken(jwt); // 解析权限
// 2. 构建用户信息(可自定义 UserDetails 实现)
UserDetails userDetails = new CustomUserDetails(userId, username, "", authorities);
// 创建认证对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置认证到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 关键:存储token到请求上下文(必须执行这一步)
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes != null) {
attributes.setAttribute("JWT_TOKEN", jwt, RequestAttributes.SCOPE_REQUEST);
// 可临时打印日志验证是否存储成功
logger.info("存储JWT_TOKEN到上下文");
} else {
logger.error("RequestAttributes为空,无法存储JWT_TOKEN");
}
}
} catch (Exception e) {
logger.error("无法设置用户认证: {}", e);
}
filterChain.doFilter(request, response);
}
修改JwtTokenProvider,增加如下方法:
java
public String generateToken(CustomUserDetails userPrincipal) {
// 提取权限字符串
List<String> roles = userPrincipal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
// 构建 JWT 声明
Map<String, Object> newClaims = new HashMap<>();
newClaims.put("userId", userPrincipal.getUserId());
newClaims.put("roles", roles);
return Jwts.builder()
.claims(newClaims)
.subject(userPrincipal.getUsername())
.issuedAt(new Date())
.expiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS256, jwtSecret)
.compact();
}
// 从 JWT 中获取用户ID
public Long getUserIdFromJwtToken(String token) {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).build().parseClaimsJws(token).getBody();
return claims.get("userId", Long.class);
}
// 从 JWT 中获取权限
public Collection<GrantedAuthority> getAuthoritiesFromJwtToken(String token) {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).build().parseClaimsJws(token).getBody();
List<String> roles = claims.get("roles", List.class);
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
服务间Feign调用传递令牌
使用了 UsernamePasswordAuthenticationToken 之后,Feign 拦截器只需稍作调整即可传递令牌。在公共模块下添加Feign令牌传递拦截器:
java
package com.waylau.rednote.common.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
/**
* FeignTokenInterceptor Feign Token Interceptor
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/28
**/
@Component
public class FeignTokenInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 适配 UsernamePasswordAuthenticationToken
if (authentication instanceof UsernamePasswordAuthenticationToken) {
// RequestAttributes 存储令牌(推荐)
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes != null) {
String token = (String) attributes.getAttribute("JWT_TOKEN", RequestAttributes.SCOPE_REQUEST);
if (token != null) {
template.header("Authorization", "Bearer " + token);
}
}
}
}
}
3.5 重构安全配置类
抽取公共安全配置,保留服务特定配置
1. 公共模块:抽取通用安全逻辑
将各服务共有的安全配置(如JWT过滤器、密码加密、无状态会话等)抽取到公共模块的src/main/java/com/waylau/rednote/common/config/BaseSecurityConfig.java中:
java
package com.waylau.rednote.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* BaseSecurityConfig 各服务共有的安全配置
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/07/28
**/
public abstract class BaseSecurityConfig {
// 公共Bean:密码加密器(所有服务通用)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 公共过滤器:JWT认证过滤器(所有服务通用)
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
// 公共配置:无状态会话、禁用CSRF等(所有服务通用)
protected void configureCommon(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
// 允许指定资源的请求不需认证
.requestMatchers("/auth/login", "/login", "/auth/register", "/css/**", "/js/**", "/fonts/**", "/images/**", "/favicon.ico").permitAll()
.requestMatchers("/error").permitAll()
// 允许管理员角色访问
.requestMatchers("/admin/**").hasRole("ADMIN")
// 允许管理员或者普通用户角色访问
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 允许普通用户角色访问
.requestMatchers("/note/**").hasRole("USER")
// 允许普通用户角色访问
.requestMatchers("/explore/**").hasRole("USER")
// 允许普通用户角色访问
.requestMatchers("/like/**").hasRole("USER")
// 允许普通用户角色访问
.requestMatchers("/comment/**").hasRole("USER")
// 允许普通用户角色访问
/*.requestMatchers("/log/**").hasRole("USER")*/
.requestMatchers("/log/**").hasAnyRole("USER", "ADMIN")
/*// 允许管理员或者普通用户角色访问
.requestMatchers("/file/**").hasAnyRole("USER", "ADMIN")*/
// 允许匿名访问静态图片资源
.requestMatchers("/uploads/**").permitAll()
.requestMatchers("/file/**").permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
)
// 禁用 CSRF
.csrf(csrf -> csrf.disable())
// 异常处理
.exceptionHandling(exception -> exception
// 指定403错误页面
.accessDeniedPage("/error/403")
)
// 无状态会话
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 启用 JWT 认证过滤器
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
;
}
// 抽象方法:留给子类实现服务特定的权限规则
protected abstract void configureAuthorize(HttpSecurity http) throws Exception;
}
原来在rednote-user-microservice模块下的CustomAuthenticationFailureHandler.java、CustomAuthenticationProvider.java、CustomUserDetails.java、JwtAuthenticationFilter.java、JwtTokenProvider.java迁移到rednote-common模块下。
2. 各领域微服务:继承基础配置,添加特定规则
用户领域服务继承公共配置,仅保留自身特有的权限规则:
java
package com.waylau.rednote.usermicroservice.infrastructure.config;
import com.waylau.rednote.common.config.BaseSecurityConfig;
import com.waylau.rednote.common.config.CustomAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;
/**
* WebSecurityConfig 安全配置类
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/05/30
**/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用@PreAuthorize等注解
public class WebSecurityConfig extends BaseSecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securedFilterChain(HttpSecurity http) throws Exception {
// 1. 应用公共配置(无状态、JWT过滤器等)
configureCommon(http);
// 2. 配置用户服务特有的权限规则
configureAuthorize(http);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
return new CustomAuthenticationProvider(userDetailsService, passwordEncoder());
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Override
protected void configureAuthorize(HttpSecurity http) throws Exception {
}
}
内容领域微服务、文件领域微服务、管理领域微服务也继承基础配置,自身的权限规则如果没有特殊配置,则可以置空:
java
import com.waylau.rednote.common.config.BaseSecurityConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
/**
* WebSecurityConfig 安全配置类
*
* @author <a href="https://waylau.com">Way Lau</a>
* @version 2025/05/30
**/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用@PreAuthorize等注解
public class WebSecurityConfig extends BaseSecurityConfig {
@Bean
public SecurityFilterChain securedFilterChain(HttpSecurity http) throws Exception {
// 1. 应用公共配置(无状态、JWT过滤器等)
configureCommon(http);
// 2. 配置用户服务特有的权限规则
configureAuthorize(http);
return http.build();
}
@Override
protected void configureAuthorize(HttpSecurity http) throws Exception {
// 如果没有特殊配置,则可以置空
}
}
总结:公共模块放"通用逻辑",服务模块放"特定规则"
-
适合放公共模块的内容:
- JWT过滤器、密码加密器等通用组件。
- 无状态会话、禁用CSRF等通用配置。
- 抽象基类(如
BaseSecurityConfig)。
-
必须留在服务模块的内容:
- 服务特定的接口路径权限规则(如
/admin/**、/note/**)。 - 依赖服务特有Bean的配置(如用户服务的
UserDetailsService)。 - 服务独有的认证/授权逻辑(如管理服务的特殊权限校验)。
- 服务特定的接口路径权限规则(如
这种设计既避免了代码重复,又保留了各服务安全配置的灵活性,是微服务架构中安全配置的最佳实践。
3.6 Seata ShouldNeverHappenException异常解决
本文主要介绍在使用Apache Seata时产生ShouldNeverHappenException异常"Get table meta failed"问题的原因,及解决方案。
原因分析
日志输出如下:
java
org.apache.seata.common.exception.ShouldNeverHappenException: [xid:172.28.192.1:8091:8872768934756659209] Get table meta failed, please check whether the table `t_note_seq` exists.
at org.apache.seata.rm.datasource.sql.struct.cache.AbstractTableMetaCache.getTableMeta(AbstractTableMetaCache.java:77) ~[seata-all-2.1.0.jar:2.1.0]
at org.apache.seata.rm.datasource.exec.BaseTransactionalExecutor.getTableMeta(BaseTransactionalExecutor.java:325) ~[seata-all-2.1.0.jar:2.1.0]
at org.apache.seata.rm.datasource.exec.BaseTransactionalExecutor.getTableMeta(BaseTransactionalExecutor.java:310) ~[seata-all-2.1.0.jar:2.1.0]
断点调试过程中出现如下异常:
bash
Caused by: org.apache.seata.common.exception.ShouldNeverHappenException: Could not found any index in the table: t_note_seq
at org.apache.seata.rm.datasource.sql.struct.cache.MysqlTableMetaCache.resultSetMetaToSchema(MysqlTableMetaCache.java:186) ~[seata-all-2.1.0.jar:2.1.0]
at org.apache.seata.rm.datasource.sql.struct.cache.MysqlTableMetaCache.fetchSchema(MysqlTableMetaCache.java:83) ~[seata-all-2.1.0.jar:2.1.0]
... 192 common frames omitted
从错误信息和你的排查结果来看,Seata 在处理 t_note_seq 表时因未找到任何索引而抛出异常。这是因为 Seata 的 AT 模式需要通过表的索引(尤其是主键索引)来生成全局锁和 undo 日志,而序列表(t_note_seq)通常设计简单,可能未显式创建索引,导致 Seata 校验失败。
解决方案:为序列表添加索引
针对序列表的特性,最直接的解决方式是 为其添加主键索引(序列表通常只有一个字段,可直接将该字段设为主键)。
序列表 t_note_seq 是 Hibernate 自动生成的序列表(如使用 @GeneratedValue(strategy = GenerationType.SEQUENCE) 或者 @GeneratedValue(strategy = GenerationType.AUTO)),可在实体类中显式指定序列表的索引策略,或手动修改框架生成的表结构。
例如,Hibernate 生成的序列表 t_note_seq 默认可能无主键,
bash
mysql> DESC t_note_seq;
+----------+--------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------+------+-----+---------+-------+
| next_val | bigint | YES | | NULL | |
+----------+--------+------+-----+---------+-------+
1 row in set (0.041 sec)
需手动添加:
sql
-- 针对 Hibernate 自动生成的序列表
ALTER TABLE t_note_seq ADD COLUMN id BIGINT AUTO_INCREMENT PRIMARY KEY; -- 新增主键列
其他的中间表也是类似处理:
sql
-- 针对 Hibernate 自动生成的序列表
ALTER TABLE note_images ADD COLUMN id BIGINT AUTO_INCREMENT PRIMARY KEY; -- 新增主键列
ALTER TABLE note_topics ADD COLUMN id BIGINT AUTO_INCREMENT PRIMARY KEY; -- 新增主键列
为什么 Seata 要求表必须有索引?
Seata AT 模式的核心逻辑是通过 全局锁 保证分布式事务的隔离性,而全局锁的生成依赖表的索引:
- 当执行
INSERT/UPDATE/DELETE时,Seata 会解析 SQL 并提取 WHERE 条件中的索引字段。 - 基于索引字段生成全局锁键(如
表名:索引字段值),用于防止并发冲突。 - 若表无任何索引,Seata 无法生成有效的全局锁键,因此强制要求表必须有至少一个索引。
总结
解决 Could not found any index in the table: t_note_seq 错误的唯一方式是 为 t_note_seq 表添加索引:
- 优先将序列字段(如
next_val)设为主键索引(最符合序列表的设计)。 - 若有特殊需求,也可添加唯一索引或普通索引。
- 确保索引添加后能被 Seata 识别(可通过
SHOW INDEX验证)。
添加索引后,Seata 可正常解析表元数据,分布式事务即可顺利执行。