1. 背景介绍
这两天我在继续完善一个 Spring Boot + MyBatis-Plus + MySQL + 静态 HTML/jQuery 的抽奖系统后台。当前主要在推进三块内容:
- 用户注册与密码登录
- 后台管理页跳转与登录态维护
- 人员列表查询与展示
问题就是在这一串前后端联调里被连续触发出来的:前端看起来"登录成功"了,但页面不跳;人员列表接口明明已经放行,却还是拿不到数据;数据库里的用户主键又莫名其妙变成了超大的雪花 ID。整个过程很典型,也很适合拿来做一次完整复盘。
2. 遭遇的困境(Bug/报错)
最开始出现的是几个连续的异常现象。
现象 1:登录提示成功,但页面不跳转
text
登录成功,Token 已保存。正在进入普通用户页面...
页面只停留在这句提示上,没有真正进入后台页面。
现象 2:人员列表接口请求被拦截
后端日志里最早出现的是 JWT 解析失败:
text
获取路径: /user/base-user/find-lis
获取token: "eyJhbGciOiJIUzI1NiJ9..."
JWT signature does not match locally computed signature.
token解析失败
这里最醒目的细节是:token 外面带了一层双引号。
现象 3:拦截器通过后,人员列表仍然报 500
当我把 token 问题修掉之后,日志继续往下走,新的异常就浮出来了:
java
java.lang.NullPointerException: Cannot invoke
"com.amadeus.lotterysystem.dao.dataobject.Encrypt.getValue()"
because the return value of
"com.amadeus.lotterysystem.dao.dataobject.UserDO.getPhoneNumber()" is null
at com.amadeus.lotterysystem.service.Impl.UserServiceImpl.lambda$findUserInfoList$0(UserServiceImpl.java:81)
也就是说:拦截器这次其实已经放行了,真正炸掉的是人员列表的数据转换逻辑。
现象 4:新注册用户的 ID 仍然是超大的雪花 ID
我后来排查数据库时发现:
sql
CREATE TABLE `user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
...
) ENGINE=InnoDB AUTO_INCREMENT=2062046654608482309
表面上看 id 已经是 AUTO_INCREMENT,但自增起点居然已经被抬到了一个巨大的值,所以后续插入的新用户仍然会拿到看起来像"雪花算法"的超大 ID。
3. 排查与分析
这轮问题不是单点故障,而是一串非常典型的"前后端联调连锁反应"。
3.1 登录成功但不跳转
我先回看了登录页 blogin.html 的成功分支。最开始的问题很简单:
- 登录成功后只是执行了
showSuccess(...) - 根本没有页面跳转逻辑
后来我补了跳转,但又引入了一个前端小坑:
javascript
var redirect = params.get('redirect');
这里使用了 params,但页面里其实没有定义这个变量,所以实际执行到跳转回调时,JS 直接抛错,导致页面仍然不跳。
3.2 JWT 被拦截的真正原因
从日志里能看出,后端拿到的是:
text
"eyJhbGciOiJIUzI1NiJ9..."
也就是带双引号的字符串,而不是原始 JWT。JWT 验签是严格按原始字符串来算的,只要多一个字符,签名就对不上。
所以这个问题的根因不是"token 无效",而是:
- 前端存储或读取 token 时把它变成了带引号的字符串
- 后端拦截器又没有做清洗
- 最终导致
SignatureException
3.3 人员列表 500 的底层原因
当拦截器问题修掉后,真正的数据问题才露出来。
人员列表查询走的是:
UserMapper.selectUserListByIdentity(...)UserServiceImpl.findUserInfoList(...)
在 findUserInfoList 里有这么一段:
java
userDTO.setPhoneNumber(userDO.getPhoneNumber().getValue());
但查询人员列表时,phoneNumber 没有被正确映射回来,导致 userDO.getPhoneNumber() 为 null。
本质原因有两个:
- 列表查询的 SQL 用的是
select * from user - 这条查询没有复用前面已经写好的
@Results / ResultMap,于是EncryptTypeHandler没有稳定生效
于是最终形成链路:
- Mapper 没把
phone_number正确映射为Encrypt - Service 层又默认它一定不为空
- 一调用
.getValue()就直接 NPE
3.4 用户主键为什么还会继续变大
这是最容易误判的一类问题。
我最开始修的是实体主键策略:
- 给
BaseDO.id增加了@TableId(type = IdType.AUTO) - 并补了 MyBatis-Plus 全局配置
id-type=auto
但数据库里还是继续插出超大 ID。重新检查后发现,问题不在"当前插入策略",而在 数据库自增计数器已经被历史雪花 ID 顶高了。
也就是说:
- 代码现在确实不再主动生成雪花 ID 了
- 但是 MySQL 自增序列已经来到
2062046654608482309 - 所以哪怕走
AUTO_INCREMENT,新数据依然会是巨大的数字
4. 终极解决方案
这一轮的解决并不是改一个点,而是把整条链路都补齐。
4.1 登录成功后补齐跳转逻辑
修改前
javascript
showSuccess('登录成功,Token 已保存。身份:' + formatIdentity(returnedIdentity));
修改后
javascript
var queryParams = new URLSearchParams(window.location.search);
showSuccess('登录成功,Token 已保存。正在进入' + formatIdentity(returnedIdentity) + '页面...');
var redirectPath = getRedirectPath(returnedIdentity);
window.setTimeout(function () {
window.top.location.href = redirectPath;
}, 300);
function getRedirectPath(identity) {
var redirect = queryParams.get('redirect');
if (redirect && redirect.charAt(0) === '/') {
return redirect;
}
if (identity === 'ADMIN') {
return '/admin.html';
}
return '/admin.html';
}
这里顺手把 window.location.href 提升成了 window.top.location.href,这样即使登录页在 iframe 里,也能推动整个页面跳转。
4.2 统一修复 token 存取与解析
前端修改前
javascript
localStorage.setItem('token', result.token);
前端修改后
javascript
var token = normalizeToken(result.token);
localStorage.setItem('token', token);
localStorage.setItem('user_token', token);
function normalizeToken(token) {
return token ? String(token).trim().replace(/^"+|"+$/g, '') : '';
}
后端拦截器修改前
java
String jwtToken = request.getHeader("user_token");
Claims claims = JWTUtil.parseJWT(jwtToken);
if (claims == null) {
return false;
}
后端拦截器修改后
java
String jwtToken = normalizeToken(request.getHeader("user_token"));
Claims claims = JWTUtil.parseJWT(jwtToken);
if (claims == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
private String normalizeToken(String token) {
if (token == null) {
return null;
}
return token.trim().replaceAll("^\"+|\"+$", "");
}
这样做的好处是:前端不再制造脏 token,后端也具备容错能力。
4.3 修复人员列表接口与数据映射
Mapper 修改前
java
@Select("<script>" +
" select * from user" +
" <if test=\"identity!=null\">" +
" where identity = #{identity}" +
" </if>" +
" order by id desc" +
" </script>")
List<UserDO> selectUserListByIdentity(@Param("identity")String identity);
Mapper 修改后
java
@Select("<script>" +
" select id, gmt_create, gmt_modified, user_name, email, phone_number, password, identity from user" +
" <if test=\"identity!=null\">" +
" where identity = #{identity}" +
" </if>" +
" order by id desc" +
" </script>")
@ResultMap("UserDOMap")
List<UserDO> selectUserListByIdentity(@Param("identity")String identity);
Service 修改前
java
userDTO.setPhoneNumber(userDO.getPhoneNumber().getValue());
Service 修改后
java
userDTO.setPhoneNumber(userDO.getPhoneNumber() == null ? null : userDO.getPhoneNumber().getValue());
同时我还修正了 Controller 的路径和返回字段:
java
@RequestMapping("/base-user/find-list")
并把列表里的 identity 从误填的 email 改回真正的身份值。
4.4 修复 MyBatis-Plus 主键策略和数据库自增计数器
实体层修改前
java
private Long id;
实体层修改后
java
@TableId(value = "id", type = IdType.AUTO)
private Long id;
配置修改后
properties
mybatis-plus.global-config.db-config.id-type=auto
但这还不够,因为数据库里已经被历史数据污染过了。
我最后还执行了数据库修复,把已有雪花 ID 用户迁回正常范围,并重置自增值:
sql
UPDATE user
SET id = CASE id
WHEN 2062046654608482306 THEN 1
WHEN 2062046654608482307 THEN 2
WHEN 2062046654608482308 THEN 3
ELSE id
END
WHERE id IN (2062046654608482306, 2062046654608482307, 2062046654608482308);
ALTER TABLE user AUTO_INCREMENT = 4;
这一步做完后,后续新注册用户的 ID 才真正恢复为 4、5、6... 这种正常递增形式。
5. 踩坑心得
5.1 联调时,不要只盯着"第一处报错"
这次非常典型:
- 先看到的是 JWT 被拦截
- 修完以后又出现 人员列表 NPE
- 再往后又挖出 数据库自增计数器异常
如果只修第一层,就会误以为"问题已经解决",但真正的业务链路其实还没跑通。完整走通一次真实请求链路,往往比只看局部代码更重要。
5.2 Token、主键策略、类型处理器,都是"系统级约定"
这几个问题表面分散,实际上都属于同一类:前后端或框架之间的约定没有统一。
- Token 存的时候和取的时候不一致
- 主键策略在实体、框架、数据库三层没有完全对齐
EncryptTypeHandler在单条查询里生效,但在列表查询里被绕开了
我的收获是:
- 认证字段命名要统一 ,比如固定只用
user_token或Authorization - 主键策略要同时检查代码和数据库,不能只改其中一边
- 自定义类型处理器一旦存在,列表查询最好显式复用统一映射 ,别偷懒写
select *
这次排障虽然来回折腾了几轮,但也把登录、拦截、列表、数据库这几条链都理顺了。回头看,其实每一步都很值,因为系统现在终于不是"某个接口能跑",而是核心流程真正被打通了。