From 70561876d6d6ec3966de9454b145aef323475264 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Sun, 16 Nov 2025 23:10:35 -0800 Subject: [PATCH] =?UTF-8?q?fix(auth):=20=E4=BF=AE=E5=A4=8D=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E9=A6=96=E6=AC=A1=E7=99=BB=E5=BD=95=E6=97=B6=E7=A9=BA?= =?UTF-8?q?=E6=8C=87=E9=92=88=E5=B4=A9=E6=BA=83=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在设备不存在分支注册后立即查询设备信息并赋值,避免后续操作解引用空指针 同时增加设备信息空指针保护并完善错误处理 --- .trae/documents/修复设备首次登录空指针崩溃.md | 67 +++++++++++++++++++ internal/logic/auth/deviceLoginLogic.go | 27 +++++++- 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 .trae/documents/修复设备首次登录空指针崩溃.md diff --git a/.trae/documents/修复设备首次登录空指针崩溃.md b/.trae/documents/修复设备首次登录空指针崩溃.md new file mode 100644 index 0000000..6f53823 --- /dev/null +++ b/.trae/documents/修复设备首次登录空指针崩溃.md @@ -0,0 +1,67 @@ +## 修复目标 +- 解决首次设备登录时在 `internal/logic/auth/deviceLoginLogic.go:99` 对 `deviceInfo` 赋值导致的空指针崩溃,确保接口稳定返回。 + +## 根因定位 +- 设备不存在分支仅创建用户与设备记录,但未为局部变量 `deviceInfo` 赋值;随后在 `internal/logic/auth/deviceLoginLogic.go:99-100` 使用 `deviceInfo` 导致 `nil` 解引用。 +- 参考位置: + - 赋值处:`internal/logic/auth/deviceLoginLogic.go:99-101` + - 设备存在分支赋值:`internal/logic/auth/deviceLoginLogic.go:88-95` + - 设备不存在分支未赋值:`internal/logic/auth/deviceLoginLogic.go:74-79` + - `UpdateDevice` 需要有效设备 `Id`:`internal/model/user/device.go:58-69` + +## 修改方案 +1. 在“设备不存在”分支注册完成后,立即通过标识重新查询设备,赋值给 `deviceInfo`: + - 在 `internal/logic/auth/deviceLoginLogic.go` 的 `if errors.Is(err, gorm.ErrRecordNotFound)` 分支中,`userInfo, err = l.registerUserAndDevice(req)` 之后追加: + - `deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)` + - 如果查询失败则返回数据库查询错误(与现有风格一致)。 +2. 在更新设备 UA 前增加空指针保护,并不再忽略更新错误: + - 将 `internal/logic/auth/deviceLoginLogic.go:99-101` 改为: + - 检查 `deviceInfo != nil` + - `deviceInfo.UserAgent = req.UserAgent` + - `if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {` 记录错误并返回包装后的错误 `xerr.DatabaseUpdateError`。 +3. 可选优化(减少二次查询): + - 将 `registerUserAndDevice(req)` 的返回值改为 `(*user.User, *user.Device, error)`,在注册时直接返回新建设备对象;调用点随之调整。若选择此方案,仍需在更新前做空指针保护。 + +## 代码示例(方案1,最小改动) +```go +// internal/logic/auth/deviceLoginLogic.go +// 设备不存在分支注册后追加一次设备查询 +userInfo, err = l.registerUserAndDevice(req) +if err != nil { + return nil, err +} +deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) +if err != nil { + l.Errorw("query device after register failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device after register failed: %v", err.Error()) +} + +// 更新 UA,不忽略更新错误 +if deviceInfo != nil { + deviceInfo.UserAgent = req.UserAgent + if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil { + l.Errorw("update device failed", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error()) + } +} +``` + +## 测试用例与验证 +- 用例1:首次设备标识登录(设备不存在)应成功返回 Token,日志包含注册与登录记录,无 500。 +- 用例2:已存在设备标识登录(设备存在)应正常更新 UA 并返回 Token。 +- 用例3:模拟数据库异常时应返回一致的业务错误码,不产生 `panic`。 + +## 风险与回滚 +- 改动限定在登录逻辑,属最小范围;若出现异常,回滚为当前版本即可。 +- 不改变数据结构与外部接口行为,兼容现有客户端。 + +## 后续优化(可选) +- 统一 `UpdateDevice` 错误处理路径,避免 `_ = ...` 静默失败。 +- 为“首次设备登录”场景补充集成测试,保证不再回归。 diff --git a/internal/logic/auth/deviceLoginLogic.go b/internal/logic/auth/deviceLoginLogic.go index 617d202..3ea802d 100644 --- a/internal/logic/auth/deviceLoginLogic.go +++ b/internal/logic/auth/deviceLoginLogic.go @@ -68,6 +68,7 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ }() // Check if device exists by identifier + createdNewDevice := false deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -76,6 +77,7 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ if err != nil { return nil, err } + createdNewDevice = true } else { l.Errorw("query device failed", logger.Field("identifier", req.Identifier), @@ -95,9 +97,28 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ } } - // 根据 req 中的UA 更新UA - deviceInfo.UserAgent = req.UserAgent - _ = l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo) + if createdNewDevice { + deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err != nil { + l.Errorw("query device after register failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device after register failed: %v", err.Error()) + } + } + + if deviceInfo != nil { + deviceInfo.UserAgent = req.UserAgent + if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil { + l.Errorw("update device failed", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error()) + } + } // Generate session id sessionId := uuidx.NewUUID().String()