xiaohongshu
xiaohongshu
bug-detective
基于若依-vue-plus框架的Bug排查与调试标准流程。提供系统化的问题诊断方法论,涵盖: 1. 日志链路追踪与TraceId全链路分析 2. 全局异常处理器的堆栈解析方法 3. 框架级常见陷阱(Long精度丢失、逻辑删除、缓存不一致、权限拦截) 4. MyBatis-Plus与Redis的排查技巧 5. 分布式环境下的事务与并发问题定位 触发场景: - 系统报错(500/403/404等HTTP错误码) - 接口返回异常数据或格式错误 - 前端显示数据与数据库不一致 - 查询结果为空或数据缺失 - 事务回滚或数据不一致 - 缓存穿透、雪崩或击穿 - 性能问题(慢查询、高CPU、OO...
Full skill instructions
核心理念:遵循"日志→异常→数据→配置→代码"的五层排查法,优先使用框架提供的诊断工具,避免盲目猜测和改动代码。
详细说明:
生产环境排查必须依赖TraceId进行全链路日志追踪。若依框架通过MDC(Mapped Diagnostic Context)自动为每个请求生成唯一TraceId,贯穿Controller→Service→Mapper全流程。
排查步骤:
X-Trace-Id中提取grep "TraceId=xxxxx" application.log查看完整调用链GlobalExceptionHandler捕获的堆栈信息,不要仅关注Controller层ServiceException中,需仔细阅读code和message字段e.getCause()追踪底层异常(如SQL异常、Redis连接异常)关键配置:
# application.yml 日志配置示例
logging:
level:
com.ruoyi: INFO
com.ruoyi.system.mapper: DEBUG # 仅对Mapper开启SQL日志
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%X{traceId}] %-5level %logger{50} - %msg%n"
/**
* 全局异常处理器 (若依标准配置)
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(ServiceException.class)
public R<Void> handleServiceException(ServiceException e, HttpServletRequest request) {
String traceId = MDC.get("traceId");
log.error("业务异常发生 [TraceId:{}] [URI:{}] [错误码:{}] [消息:{}]",
traceId, request.getRequestURI(), e.getCode(), e.getMessage());
// 必须返回错误码和具体错误信息,而非直接暴露堆栈
return R.fail(e.getCode(), e.getMessage());
}
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e, HttpServletRequest request) {
String traceId = MDC.get("traceId");
log.error("系统异常发生 [TraceId:{}] [URI:{}]", traceId, request.getRequestURI(), e);
// 生产环境不暴露详细堆栈,记录traceId供后续排查
return R.fail("系统内部错误,请联系管理员(TraceId:" + traceId + ")");
}
/**
* 数据库异常(常见于SQL语法错误、字段不存在)
*/
@ExceptionHandler(SQLException.class)
public R<Void> handleSQLException(SQLException e, HttpServletRequest request) {
String traceId = MDC.get("traceId");
log.error("数据库异常 [TraceId:{}] [SQLState:{}] [ErrorCode:{}]",
traceId, e.getSQLState(), e.getErrorCode(), e);
return R.fail("数据操作失败(TraceId:" + traceId + ")");
}
}
详细说明: 若依框架基于MyBatis-Plus和SpringBoot构建,存在几类高频Bug模式。排查数据异常时,需按以下优先级检查:
现象:前端显示的ID末尾变为00,导致根据ID查询失败
原因:JavaScript的Number类型最大安全整数为2^53-1(16位),Java的Long为64位
解决方案:
// 方案1:全局配置(推荐)
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
// 所有Long类型自动转为String
builder.serializerByType(Long.class, ToStringSerializer.instance);
builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
};
}
}
// 方案2:实体类单独标注
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
public class SysUser extends BaseEntity {
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
@JsonSerialize(using = ToStringSerializer.class)
private Long deptId;
}
排查命令:
// 浏览器控制台验证
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(userId > Number.MAX_SAFE_INTEGER); // true则必定丢失精度
现象:数据库有数据,但查询结果为空
原因:若依默认使用@TableLogic实现软删除,MyBatis-Plus会自动在SQL添加WHERE del_flag = 0
解决方案:
// 排查逻辑删除配置
@TableName(value = "sys_user")
public class SysUser {
// 若依默认逻辑删除字段:0未删除 2已删除
// 如果查询结果为空,检查数据库该字段值是否被意外置为2
@TableLogic(value = "0", delval = "2")
private String delFlag;
}
排查步骤:
@TableLogic注解SELECT * FROM sys_user WHERE user_id = ? -- 不经过MyBatis-Plusdel_flag字段值:若为2则表示已逻辑删除includedDeleted())// 需要查询已删除数据的场景
List<SysUser> users = userMapper.selectList(
Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getUserName, "admin")
// 包含已删除数据
.apply("1=1") // 绕过逻辑删除(不推荐)
);
// 推荐方式:使用原生SQL
@Select("SELECT * FROM sys_user WHERE user_name = #{userName}")
List<SysUser> selectIncludingDeleted(@Param("userName") String userName);
现象:接口返回403 Forbidden,但用户已登录
原因:若依的@PreAuthorize权限注解校验失败
解决方案:
// 排查权限配置
@RestController
@RequestMapping("/system/user")
public class SysUserController {
// 检查权限标识是否与菜单配置一致
@PreAuthorize("@ss.hasPermi('system:user:query')")
@GetMapping("/list")
public R<List<SysUser>> list() {
// ...
}
}
排查步骤:
SELECT * FROM sys_role_menu WHERE role_id = ? AND menu_id = ?SELECT perms FROM sys_menu WHERE menu_id = ?@PreAuthorize值与数据库perms字段是否一致GET login_tokens:{token} 确认用户权限列表现象:修改数据后,查询仍返回旧数据
原因:Redis缓存未及时更新或删除
解决方案:
// 正确的缓存更新模式
@Service
public class SysUserServiceImpl implements ISysUserService {
@Autowired
private RedisCache redisCache;
private static final String USER_CACHE_KEY = "user:info:";
@Override
@Transactional
public int updateUser(SysUser user) {
int rows = userMapper.updateById(user);
if (rows > 0) {
// 先更新数据库,后删除缓存(Cache-Aside模式)
redisCache.deleteObject(USER_CACHE_KEY + user.getUserId());
}
return rows;
}
@Override
public SysUser selectUserById(Long userId) {
// 先查缓存,缓存未命中再查数据库
String cacheKey = USER_CACHE_KEY + userId;
SysUser user = redisCache.getCacheObject(cacheKey);
if (user == null) {
user = userMapper.selectById(userId);
if (user != null) {
// 设置缓存过期时间,防止永久占用内存
redisCache.setCacheObject(cacheKey, user, 30, TimeUnit.MINUTES);
}
}
return user;
}
}
排查命令:
# Redis CLI 检查缓存
redis-cli
> KEYS user:info:*
> GET user:info:1
> TTL user:info:1 # 检查过期时间
> DEL user:info:1 # 手动删除测试
详细说明: 性能问题往往源于低效的SQL查询,特别是循环中的单条查询(N+1问题)。
排查步骤:
# application-dev.yml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
// ❌ 错误示例:N+1查询
List<SysUser> users = userMapper.selectList(null);
for (SysUser user : users) {
// 每次循环都查询一次数据库!
SysDept dept = deptMapper.selectById(user.getDeptId());
user.setDept(dept);
}
// ✅ 正确示例:批量查询或关联查询
List<SysUser> users = userMapper.selectUserListWithDept();
// 或使用MyBatis-Plus的批量查询
List<Long> deptIds = users.stream()
.map(SysUser::getDeptId)
.distinct()
.collect(Collectors.toList());
List<SysDept> depts = deptMapper.selectBatchIds(deptIds);
Map<Long, SysDept> deptMap = depts.stream()
.collect(Collectors.toMap(SysDept::getDeptId, dept -> dept));
users.forEach(user -> user.setDept(deptMap.get(user.getDeptId())));
<!-- SysUserMapper.xml -->
<select id="selectUserListWithDept" resultType="SysUser">
SELECT u.*, d.dept_name
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
WHERE u.del_flag = '0'
</select>
详细说明:
@Transactional注解失效是常见Bug,导致数据不一致。
失效场景与解决方案:
// ❌ 事务不生效
@Transactional
private void updateUser(SysUser user) { }
// ✅ 必须是public方法
@Transactional
public void updateUser(SysUser user) { }
@Service
public class UserService {
// ❌ 事务不生效(自调用问题)
public void outerMethod() {
this.innerMethod(); // 直接调用,未经过代理
}
@Transactional
public void innerMethod() {
// 事务失效
}
// ✅ 解决方案1:拆分到不同类
@Autowired
private UserTransactionService transactionService;
public void outerMethod() {
transactionService.innerMethod(); // 经过Spring代理
}
// ✅ 解决方案2:注入自身代理
@Autowired
@Lazy
private UserService self;
public void outerMethod() {
self.innerMethod();
}
}
// ❌ 事务不回滚
@Transactional
public void updateUser(SysUser user) {
try {
userMapper.updateById(user);
int i = 1 / 0; // 触发异常
} catch (Exception e) {
log.error("更新失败", e);
// 异常被吞掉,事务不回滚!
}
}
// ✅ 解决方案1:重新抛出异常
@Transactional
public void updateUser(SysUser user) {
try {
userMapper.updateById(user);
int i = 1 / 0;
} catch (Exception e) {
log.error("更新失败", e);
throw new ServiceException("用户更新失败");
}
}
// ✅ 解决方案2:手动回滚
@Transactional
public void updateUser(SysUser user) {
try {
userMapper.updateById(user);
int i = 1 / 0;
} catch (Exception e) {
log.error("更新失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return;
}
}
// ❌ 检查异常不回滚
@Transactional
public void updateUser(SysUser user) throws Exception {
userMapper.updateById(user);
throw new Exception("检查异常"); // 事务不回滚!
}
// ✅ 指定回滚异常类型
@Transactional(rollbackFor = Exception.class)
public void updateUser(SysUser user) throws Exception {
userMapper.updateById(user);
throw new Exception("检查异常");
}
详细说明: 前后端数据格式不一致、时间时区偏差、空值处理等问题的排查。
现象:前端显示时间与数据库相差8小时
原因:时区配置不一致或序列化格式错误
解决方案:
// 全局配置时区和日期格式
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
// 设置时区为东八区
builder.timeZone(TimeZone.getTimeZone("GMT+8"));
// 设置日期格式
builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
};
}
}
// 实体类单独配置
public class SysUser extends BaseEntity {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
# application.yml 配置
spring:
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
现象:前端收到null字段,导致解析错误
原因:后端未配置空值序列化策略
解决方案:
// 全局配置空值处理
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
// null值不返回
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
// 或将null转为空字符串/空数组
// builder.serializationInclusion(JsonInclude.Include.ALWAYS);
};
}
}
// 实体类单独配置
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SysUser extends BaseEntity {
private String nickName;
}
详细说明: MyBatis-Plus分页插件配置错误、分页参数传递问题。
常见问题与解决方案:
// ❌ 错误示例:分页插件未生效
@GetMapping("/list")
public R<List<SysUser>> list(PageQuery pageQuery) {
// 直接查询,无分页效果
List<SysUser> users = userMapper.selectList(null);
return R.ok(users);
}
// ✅ 正确示例1:使用PageHelper(若依默认)
@GetMapping("/list")
public TableDataInfo<SysUser> list(SysUser user, PageQuery pageQuery) {
// PageHelper会自动拦截下一条查询并添加分页
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
}
// ✅ 正确示例2:使用MyBatis-Plus分页
@GetMapping("/list")
public R<Page<SysUser>> list(PageQuery pageQuery) {
Page<SysUser> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
Page<SysUser> result = userMapper.selectPage(page, null);
return R.ok(result);
}
分页插件配置:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 设置数据库类型
paginationInnerInterceptor.setDbType(DbType.MYSQL);
// 设置单页最大限制数量
paginationInnerInterceptor.setMaxLimit(500L);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
try { ... } catch (Exception e) {}不记录日志,导致问题无法追溯for循环中单条执行数据库查询或更新(N+1问题),这是性能Bug的根源@Transactional默认只回滚RuntimeException,检查异常需手动指定rollbackForlogging.level.root设置为DEBUG,会产生海量日志导致IO阻塞和磁盘爆满${}拼接参数,必须使用#{}预编译@TableLogic可能导致查询到已删除数据application.yml配置,应先分析日志WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2026-01-26'会导致索引失效NOT IN或拆分为>和<若依框架核心配置与工具类文件路径:
ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.javaruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.javaruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.javaruoyi-common/src/main/java/com/ruoyi/common/config/JacksonConfig.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.javaruoyi-system/src/main/java/com/ruoyi/system/domain/SysUser.javaruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusConfig.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusMetaObjectHandler.javaruoyi-common/src/main/java/com/ruoyi/common/utils/redis/RedisCache.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.javaruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.javaGlobalExceptionHandler的异常堆栈e.getCause())del_flag)application.yml的数据库连接配置@PreAuthorize与数据库perms字段)@Transactional是否生效EXPLAIN分析了慢SQL的执行计划INFO stats)# 根据TraceId过滤日志
grep "TraceId=xxxxx" application.log
# 查看最近的错误日志
tail -f application.log | grep "ERROR"
# 统计某个异常的出现次数
grep "ServiceException" application.log | wc -l
-- 检查逻辑删除状态
SELECT user_id, user_name, del_flag FROM sys_user WHERE user_id = ?;
-- 分析SQL执行计划
EXPLAIN SELECT * FROM sys_user WHERE user_name = 'admin';
-- 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query%';
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
# 连接Redis
redis-cli
# 查看所有key
KEYS *
# 查看某个key的值和过期时间
GET user:info:1
TTL user:info:1
# 查看缓存命中率
INFO stats
# 清空某个前缀的key
redis-cli --scan --pattern "user:info:*" | xargs redis-cli DEL
-- 查询用户的所有角色
SELECT r.* FROM sys_role r
INNER JOIN sys_user_role ur ON r.role_id = ur.role_id
WHERE ur.user_id = ?;
-- 查询角色的所有权限
SELECT m.perms FROM sys_menu m
INNER JOIN sys_role_menu rm ON m.menu_id = rm.menu_id
WHERE rm.role_id = ?;
-- 查询用户的所有权限(完整链路)
SELECT DISTINCT m.perms FROM sys_menu m
INNER JOIN sys_role_menu rm ON m.menu_id = rm.menu_id
INNER JOIN sys_user_role ur ON rm.role_id = ur.role_id
WHERE ur.user_id = ? AND m.perms IS NOT NULL;
# 下载并启动Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 监控方法执行耗时
trace com.ruoyi.system.service.impl.SysUserServiceImpl selectUserList
# 查看方法参数和返回值
watch com.ruoyi.system.service.impl.SysUserServiceImpl selectUserList "{params,returnObj}" -x 2
# 反编译类文件
jad com.ruoyi.system.service.impl.SysUserServiceImpl
<!-- pom.xml 添加依赖 -->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
# application.yml 配置
spring:
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://localhost:3306/ry-vue
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加性能分析插件(仅开发环境)
if (environment.acceptsProfiles(Profiles.of("dev"))) {
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
performanceInterceptor.setMaxTime(1000); // 超过1秒的SQL输出
performanceInterceptor.setFormat(true); // 格式化SQL
// 注意:MyBatis-Plus 3.x 已移除此插件,建议使用p6spy
}
return interceptor;
}
}
遵循本规范的五层排查法:
核心原则:先看日志,再查数据,后改代码;优先使用框架工具,避免盲目猜测。
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience