Claude-skill-registry bug-detective
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/bug-detective" ~/.claude/skills/majiayu000-claude-skill-registry-bug-detective && rm -rf "$T"
manifest:
skills/data/bug-detective/SKILL.mdsource content
Bug 排查与调试规范
核心理念:遵循"日志→异常→数据→配置→代码"的五层排查法,优先使用框架提供的诊断工具,避免盲目猜测和改动代码。
核心规范
规范1:日志链路追踪与全局异常解析
详细说明: 生产环境排查必须依赖
TraceId进行全链路日志追踪。若依框架通过MDC(Mapped Diagnostic Context)自动为每个请求生成唯一TraceId,贯穿Controller→Service→Mapper全流程。
排查步骤:
- 获取TraceId:从日志文件或响应头
中提取X-Trace-Id - 过滤完整链路:使用
查看完整调用链grep "TraceId=xxxxx" application.log - 定位异常层级:优先查看
捕获的堆栈信息,不要仅关注Controller层GlobalExceptionHandler - 解析业务异常:若依框架异常通常封装在
中,需仔细阅读ServiceException
和code
字段message - 检查嵌套异常:使用
追踪底层异常(如SQL异常、Redis连接异常)e.getCause()
关键配置:
# 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 + ")"); } }
规范2:常见框架级Bug模式识别
详细说明: 若依框架基于MyBatis-Plus和SpringBoot构建,存在几类高频Bug模式。排查数据异常时,需按以下优先级检查:
2.1 Long精度丢失问题
现象:前端显示的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则必定丢失精度
2.2 逻辑删除陷阱
现象:数据库有数据,但查询结果为空
原因:若依默认使用
@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-Plus - 检查
字段值:若为2则表示已逻辑删除del_flag - 确认业务逻辑:是否应该查询已删除数据(需使用
)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);
2.3 权限拦截403问题
现象:接口返回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 - 检查Redis缓存:
确认用户权限列表GET login_tokens:{token}
2.4 缓存不一致问题
现象:修改数据后,查询仍返回旧数据
原因: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 # 手动删除测试
规范3:数据库慢查询与N+1问题
详细说明: 性能问题往往源于低效的SQL查询,特别是循环中的单条查询(N+1问题)。
排查步骤:
- 开启SQL日志:
# application-dev.yml mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
- 识别N+1问题:
// ❌ 错误示例: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())));
- 使用MyBatis-Plus关联查询:
<!-- 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>
规范4:事务失效问题
详细说明:
@Transactional注解失效是常见Bug,导致数据不一致。
失效场景与解决方案:
4.1 非public方法
// ❌ 事务不生效 @Transactional private void updateUser(SysUser user) { } // ✅ 必须是public方法 @Transactional public void updateUser(SysUser user) { }
4.2 同类方法调用
@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(); } }
4.3 异常被捕获
// ❌ 事务不回滚 @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; } }
4.4 错误的异常类型
// ❌ 检查异常不回滚 @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("检查异常"); }
规范5:前后端数据交互问题
详细说明: 前后端数据格式不一致、时间时区偏差、空值处理等问题的排查。
5.1 日期时间格式问题
现象:前端显示时间与数据库相差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
5.2 空值处理问题
现象:前端收到
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; }
规范6:分页查询问题
详细说明: 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) {} - ❌ 禁止循环查询数据库:在
循环中单条执行数据库查询或更新(N+1问题),这是性能Bug的根源for - ❌ 禁止不指定事务回滚异常类型:
默认只回滚@Transactional
,检查异常需手动指定RuntimeExceptionrollbackFor - ❌ 禁止直接返回敏感异常信息:生产环境不能将SQL异常、堆栈信息直接暴露给前端
- ❌ 禁止在事务方法中捕获异常不抛出:会导致事务不回滚,数据不一致
配置层面
- ❌ 禁止生产环境开启DEBUG日志:将
设置为logging.level.root
,会产生海量日志导致IO阻塞和磁盘爆满DEBUG - ❌ 禁止关闭SQL注入防护:不要在Mapper中使用
拼接参数,必须使用${}
预编译#{} - ❌ 禁止缓存无过期时间:Redis缓存必须设置合理的过期时间,防止内存泄漏
- ❌ 禁止忽略逻辑删除注解:直接使用原生SQL绕过
可能导致查询到已删除数据@TableLogic
排查层面
- ❌ 禁止没有TraceId就开始排查:生产问题必须先定位TraceId,获取完整调用链
- ❌ 禁止仅看Controller层日志:异常可能在Service或Mapper层抛出,需全链路排查
- ❌ 禁止随意修改配置:排查问题时不要轻易修改
配置,应先分析日志application.yml - ❌ 禁止直连生产数据库修改数据:排查时只能SELECT查询,修改数据需走正规变更流程
数据库层面
- ❌ **禁止SELECT ***:查询时应明确指定字段,特别是有大字段(TEXT、BLOB)时
- ❌ 禁止在WHERE条件中使用函数:如
会导致索引失效WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2026-01-26' - ❌ 禁止使用!=或<>:这两个操作符无法使用索引,应改为
或拆分为NOT IN
和>< - ❌ 禁止JOIN过多表:超过3个表的JOIN会导致性能急剧下降,考虑拆分查询或冗余设计
参考代码路径
若依框架核心配置与工具类文件路径:
异常处理相关
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.java
序列化配置相关
ruoyi-common/src/main/java/com/ruoyi/common/config/JacksonConfig.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java
实体类相关
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUser.javaruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
MyBatis-Plus配置
ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusConfig.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusMetaObjectHandler.java
Redis缓存相关
ruoyi-common/src/main/java/com/ruoyi/common/utils/redis/RedisCache.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java
权限校验相关
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
检查清单(Bug排查必查项)
第一层:日志排查
- 是否获取了TraceId并过滤出完整调用链日志
- 是否查看了
的异常堆栈GlobalExceptionHandler - 是否检查了日志中的SQL语句(开启MyBatis日志)
- 是否确认了异常发生的具体层级(Controller/Service/Mapper)
- 是否检查了嵌套异常的底层原因(
)e.getCause()
第二层:异常排查
- 是否确认了异常类型(业务异常/系统异常/数据库异常)
- 是否检查了异常的错误码和错误信息
- 是否排查了异常是否被捕获后未重新抛出
- 是否确认了异常是否触发了事务回滚
- 是否检查了异常是否与权限拦截有关(403错误)
第三层:数据排查
- 是否排查了Long类型的序列化配置(精度丢失)
- 是否检查了逻辑删除字段状态(
)del_flag - 是否确认了数据库原始数据是否存在(直接SELECT查询)
- 是否检查了缓存与数据库的数据一致性
- 是否排查了日期时间的时区和格式问题
- 是否检查了空值处理配置(null序列化)
第四层:配置排查
- 是否检查了
的数据库连接配置application.yml - 是否确认了Redis连接配置和缓存过期时间
- 是否检查了MyBatis-Plus的全局配置(逻辑删除、分页插件)
- 是否确认了Jackson的序列化配置(时区、日期格式、Long转String)
- 是否检查了权限配置(
与数据库@PreAuthorize
字段)perms
第五层:代码排查
- 是否确认了事务注解
是否生效@Transactional - 是否排查了N+1查询问题(循环中的单条查询)
- 是否检查了分页插件是否正确配置和使用
- 是否确认了SQL语句的索引使用情况(EXPLAIN分析)
- 是否检查了并发问题(乐观锁/悲观锁/分布式锁)
性能问题专项排查
- 是否使用
分析了慢SQL的执行计划EXPLAIN - 是否检查了是否存在全表扫描(type=ALL)
- 是否确认了索引是否失效(未使用index)
- 是否排查了是否存在JOIN过多表的情况
- 是否检查了Redis缓存命中率(
)INFO stats - 是否使用JVM工具排查了内存泄漏(jmap、MAT)
快速诊断命令
日志排查命令
# 根据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 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
权限排查SQL
-- 查询用户的所有角色 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;
高级排查技巧
1. 使用Arthas诊断JVM问题
# 下载并启动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
2. 使用MyBatis日志插件
<!-- 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
3. 开启SQL性能分析
@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; } }
总结
遵循本规范的五层排查法:
- 日志层:获取TraceId,过滤完整调用链
- 异常层:分析异常类型、错误码、堆栈信息
- 数据层:检查数据库原始数据、缓存数据、逻辑删除状态
- 配置层:确认框架配置、序列化配置、权限配置
- 代码层:排查事务、N+1查询、索引使用
核心原则:先看日志,再查数据,后改代码;优先使用框架工具,避免盲目猜测。