package auth import ( "context" "log/slog" "strings" "hyapp/pkg/appcode" "hyapp/pkg/logx" authdomain "hyapp/services/user-service/internal/domain/auth" ) // recordUserDevice 在认证成功后合并当前设备档案。 // 该写入是 best-effort:device_id 为空(老客户端/后台调用)直接跳过;写失败只记日志, // 绝不让已经成功的登录/注册/refresh 因为设备画像落库失败而回吐错误。 func (s *Service) recordUserDevice(ctx context.Context, meta Meta, userID int64, deviceID string) { deviceID = strings.TrimSpace(deviceID) if s.authRepository == nil || userID <= 0 || deviceID == "" { return } nowMs := s.now().UnixMilli() device := authdomain.UserDevice{ AppCode: appcode.Normalize(meta.AppCode), UserID: userID, DeviceID: truncateDeviceField(deviceID, 128), Platform: truncateDeviceField(normalizePlatform(meta.Platform), 32), DeviceModel: truncateDeviceField(meta.DeviceModel, 128), Manufacturer: truncateDeviceField(meta.DeviceManufacturer, 64), OSVersion: truncateDeviceField(meta.OSVersion, 64), IMEI: truncateDeviceField(meta.IMEI, 64), AppVersion: normalizeAuditClientVersion(meta.AppVersion), BuildNumber: normalizeAuditClientVersion(meta.BuildNumber), // first_seen 只在 INSERT 生效;ON DUPLICATE KEY 更新时只有 last_seen/updated 前移。 FirstSeenAtMs: nowMs, LastSeenAtMs: nowMs, CreatedAtMs: nowMs, UpdatedAtMs: nowMs, } if err := s.authRepository.UpsertUserDevice(ctx, device); err != nil { // 失败必须可见:设备 tab 静默缺数据比登录多一条错误日志更难排查。 logx.Error( logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", device.AppCode)), "user_device_upsert_failed", err, slog.String("component", "user_auth"), slog.Int64("user_id", userID), slog.String("device_id", device.DeviceID), ) } } // deviceMetaWithRegistration 用注册 body 的设备字段补齐 Meta 缺口。 // 设备资料有两个来源:gateway 设备头(新客户端全局注入)和注册 body(旧协议字段); // 任意一侧可能单独缺失,落档案前统一取“先 header 后 body”的非空值。 func deviceMetaWithRegistration(meta Meta, registration authdomain.ThirdPartyRegistration) Meta { meta.Platform = firstNonEmpty(meta.Platform, registration.Platform) meta.DeviceModel = firstNonEmpty(meta.DeviceModel, registration.Device) meta.OSVersion = firstNonEmpty(meta.OSVersion, registration.OSVersion) meta.AppVersion = firstNonEmpty(meta.AppVersion, registration.AppVersion) meta.BuildNumber = firstNonEmpty(meta.BuildNumber, registration.BuildNumber) return meta } // truncateDeviceField 按列宽截断客户端可控字符串,超长上报不能让整条设备档案 INSERT 失败。 func truncateDeviceField(value string, maxRunes int) string { value = strings.TrimSpace(value) runes := []rune(value) if len(runes) <= maxRunes { return value } return string(runes[:maxRunes]) }