diff --git a/doc/api/log_message_report.md b/doc/api/log_message_report.md new file mode 100644 index 0000000..a9e1fcb --- /dev/null +++ b/doc/api/log_message_report.md @@ -0,0 +1,101 @@ +# 客户端错误上报接口文档 + +## 接口概览 +- 路径:`POST /v1/common/log/message/report` +- 说明:APP/PC/Web 客户端错误与异常信息上报,服务端入库 `log_message` 表,并提供管理端查询。 +- 认证:无需登录;若走设备安全通道需使用 `Login-Type: device` 与 AES 加密。 +- 中间件:`DeviceMiddleware`(当请求头 `Login-Type=device` 时启用加解密)。 + +## 请求 +- Headers: + - `Content-Type: application/json` + - 可选:`Login-Type: device`(启用设备加密通道) +- Body(JSON): + - `platform` string 必填,客户端平台,如 `android`/`ios`/`windows`/`mac`/`web` + - `appVersion` string 可选,应用版本,如 `2.4.1` + - `osName` string 可选,系统名称,如 `Android`/`iOS`/`Windows`/`macOS` + - `osVersion` string 可选,系统版本,如 `14` + - `deviceId` string 可选,设备唯一标识 + - `sessionId` string 可选,会话标识 + - `level` uint8 可选,日志等级:`1=fatal`、`2=error`、`3=warn`、`4=info`(默认 `3`) + - `errorCode` string 可选,业务或系统错误码 + - `message` string 必填,错误简述(服务器将超过约 64KB 的内容截断) + - `stack` string 可选,堆栈信息(服务器将超过约 1MB 的内容截断) + - `context` object 可选,扩展上下文(如接口路径、参数、网络状态等) + - `occurredAt` int64 可选,客户端发生时间,毫秒时间戳 + +- 服务器侧自动填充: + - `client_ip`、`user_agent`、`locale` 由请求解析 + - `user_id` 仅在鉴权后由服务端注入(本接口默认匿名) + +## 加密通道(设备) +- 当使用设备安全通道时(`Login-Type: device`),请求体需要将原始 JSON 加密为: + - `data` string:AES 加密后的密文 + - `time` string:IV/nonce(与密文配套) +- 服务端会自动解密为明文 JSON,再进行字段校验与入库。 + +## 响应 +- 成功: +``` +{ + "code": 0, + "msg": "OK", + "data": { "id": 123 } +} +``` +- 常见错误: + - `{"code":401,"msg":"TooManyRequests"}` 触发速率限制(设备ID或IP维度) + - `{"code":400,"msg":"InvalidParams"}` 参数校验失败(缺少必填或类型不符) + - `{"code":10001,"msg":"DatabaseQueryError"}` 数据库操作异常 + +## 速率限制 +- 默认按 `deviceId` 或 `client_ip` 每分钟最多约 `120` 条,超限即返回 `TooManyRequests`。 + +## 去重策略 +- 服务端计算 `digest = sha256(message|stack|errorCode|appVersion|platform)` 并尝试唯一入库,重复日志可能返回已存在记录的 `id`。 + +## 示例 +- 明文上报(推荐测试使用): +``` +curl -X POST http://localhost:8080/v1/common/log/message/report \ + -H 'Content-Type: application/json' \ + -d '{ + "platform": "android", + "appVersion": "2.4.1", + "osName": "Android", + "osVersion": "14", + "deviceId": "and-9a7f3e2c-01", + "sessionId": "sess-73f8a2a4", + "level": 2, + "errorCode": "ORDER_RENEWAL_TIMEOUT", + "message": "订单续费接口超时:/v1/public/order/renewal", + "stack": "TimeoutException: request exceeded 8000ms\\n at HttpClient.post(HttpClient.kt:214)\\n at RenewalRepository.submit(RenewalRepository.kt:87)", + "context": { + "api": "/v1/public/order/renewal", + "method": "POST", + "endpoint": "https://api.example.com/v1/public/order/renewal", + "httpStatus": 504, + "responseTimeMs": 8123, + "retryCount": 2, + "network": { "type": "cellular", "carrier": "China Mobile" } + }, + "occurredAt": 1733200005123 + }' +``` + +- 设备加密上报(示意): +``` +# 将原始JSON通过AES加密得到密文data与随机IV time +curl -X POST http://localhost:8080/v1/common/log/message/report \ + -H 'Content-Type: application/json' \ + -H 'Login-Type: device' \ + -d '{"data":"","time":""}' +``` + +## 管理端查询(用于联调验证) +- 列表:`GET /v1/admin/log/message/error/list`(需 `Authorization`) +- 详情:`GET /v1/admin/log/message/error/detail?id=...` + +## 备注 +- 字段尽量避免携带敏感信息(密码、密钥、完整令牌等);如需调试请截断或脱敏后上报。 +- 建议在 APP/PC 端统一封装上报模块,包含:字段收集、级别过滤、采样策略、离线缓存与重试、速率限制配合。 diff --git a/doc/说明文档.md b/doc/说明文档.md index 6235307..4368af2 100644 --- a/doc/说明文档.md +++ b/doc/说明文档.md @@ -25,9 +25,7 @@ - 待办:根据运营需求调整限流阈值与日志保留策略。 ## 接口规范 -- 上报:`POST /v1/common/log/message/report` - - 请求:platform、appVersion、osName、osVersion、deviceId、level、errorCode、message、stack、context、occurredAt - - 响应:`{ code, msg, data: { id } }` +- 上报:`POST /v1/common/log/message/report`(详见 `doc/api/log_message_report.md`) - 管理端列表:`GET /v1/admin/log/message/error/list` - 筛选:platform、level、user_id、device_id、error_code、keyword、start、end;分页:page、size - 响应:`{ total, list }` diff --git a/internal/logic/public/user/unbindDeviceLogic.go b/internal/logic/public/user/unbindDeviceLogic.go index 3c8d135..6e52bd8 100644 --- a/internal/logic/public/user/unbindDeviceLogic.go +++ b/internal/logic/public/user/unbindDeviceLogic.go @@ -53,21 +53,14 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { logger.Field("user_id", u.Id)) start := time.Now() err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { - // 1. 查询设备记录 - var device user.Device - err = tx.Model(&device).Where("id = ?", req.Id).First(&device).Error - if err != nil { + var dev user.Device + if err := tx.Model(&dev).Where("id = ?", req.Id).First(&dev).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "find device err: %v", err) } - - // 2. 查询对应的认证记录 var userAuth user.AuthMethods - err = tx.Model(&userAuth).Where("auth_identifier = ? and auth_type = ?", device.Identifier, "device").First(&userAuth).Error - if err != nil { + if err := tx.Model(&userAuth).Where("auth_identifier = ? and auth_type = ?", dev.Identifier, "device").First(&userAuth).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth method err: %v", err) } - - // 3. 创建新用户(匿名用户) newUser := &user.User{ Salt: "default", OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, @@ -75,65 +68,43 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { if err := tx.Create(newUser).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err) } - - // 生成并更新邀请码 newUser.ReferCode = uuidx.UserInviteCode(newUser.Id) if err := tx.Model(&user.User{}).Where("id = ?", newUser.Id).Update("refer_code", newUser.ReferCode).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) } - - // 3.2 记录注册日志 registerLog := log.Register{ AuthMethod: "device", - Identifier: device.Identifier, - RegisterIP: device.Ip, - UserAgent: device.UserAgent, + Identifier: dev.Identifier, + RegisterIP: dev.Ip, + UserAgent: dev.UserAgent, Timestamp: time.Now().UnixMilli(), } content, _ := registerLog.Marshal() - if err := tx.Create(&log.SystemLog{ + _ = tx.Create(&log.SystemLog{ Type: log.TypeRegister.Uint8(), Date: time.Now().Format("2006-01-02"), ObjectID: newUser.Id, Content: string(content), - }).Error; err != nil { - l.Errorw("failed to insert register log", - logger.Field("user_id", newUser.Id), - logger.Field("error", err.Error()), - ) - // Log error but don't fail transaction - } - - // 4. 迁移设备和认证记录到新用户 - // 更新设备归属 - if err := tx.Model(&user.Device{}).Where("id = ?", device.Id).Update("user_id", newUser.Id).Error; err != nil { + }).Error + if err := tx.Model(&user.Device{}).Where("id = ?", dev.Id).Update("user_id", newUser.Id).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device owner failed: %v", err) } - - // 更新认证归属 if err := tx.Model(&user.AuthMethods{}).Where("id = ?", userAuth.Id).Update("user_id", newUser.Id).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update auth owner failed: %v", err) } - - // 5. 检查原用户是否还有其他认证方式,如果没有则删除原用户 var count int64 - err = tx.Model(user.AuthMethods{}).Where("user_id = ?", device.UserId).Count(&count).Error - if err != nil { + if err := tx.Model(user.AuthMethods{}).Where("user_id = ?", dev.UserId).Count(&count).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count user auth methods err: %v", err) } - if count < 1 { - if err := tx.Where("id = ?", device.UserId).Delete(&user.User{}).Error; err != nil { + if err := tx.Where("id = ?", dev.UserId).Delete(&user.User{}).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete old user failed: %v", err) } } - - // 6. 清理缓存 l.Infow("设备解绑并迁移成功", - logger.Field("device_identifier", device.Identifier), - logger.Field("old_user_id", device.UserId), + logger.Field("device_identifier", dev.Identifier), + logger.Field("old_user_id", dev.UserId), logger.Field("new_user_id", newUser.Id)) - return nil }) if err != nil {