"use client"; /** * 设备 ID 工具类 * * 生产环境使用 fingerprintjs 生成客户端设备指纹。 * 非生产环境使用本地 UUID,避免开发 / 预发布调试依赖真实设备指纹。 * * * 注:仅在浏览器环境可用;SSR/Server Components 中调用会抛错。 */ import { AuthStorage } from "../data/storage/auth/auth_storage"; import { AppEnvUtil } from "./app-env"; export class DeviceIdentifier { private cachedId: string | null = null; private initPromise: Promise | null = null; /** * 获取或生成设备 ID * * 优先级:内存缓存 → localStorage 缓存 → 非生产 UUID / 生产 fingerprintjs * 并发安全:多个并发调用会共享同一个初始化 Promise */ async getDeviceId(): Promise { if (this.cachedId) return this.cachedId; if (this.initPromise) return this.initPromise; this.initPromise = this.initialize(); this.cachedId = await this.initPromise; return this.cachedId; } private async initialize(): Promise { // 1. 优先从 localStorage 读取 const storedR = await AuthStorage.getInstance().getDeviceId(); if (storedR.success && storedR.data && storedR.data.length > 0) { return storedR.data; } // 2. SSR/Node 环境守卫:调用方必须保证在浏览器中调用 if (typeof window === "undefined") { throw new Error( "DeviceIdentifier only works in browser environment. " + "Call getDeviceId() from a Client Component or browser code." ); } // 3. 非生产环境不使用真实设备指纹,生成并持久化一个本地 UUID 即可。 if (!AppEnvUtil.isProduction()) { const deviceId = createUuidV4(); await AuthStorage.getInstance().setDeviceId(deviceId); return deviceId; } // 4. 生产环境动态加载 fingerprintjs(避免 Next.js SSR 打包分析时执行) const { load } = await import("@fingerprintjs/fingerprintjs"); const fp = await load(); const result = await fp.get(); // 5. 持久化 await AuthStorage.getInstance().setDeviceId(result.visitorId); return result.visitorId; } } function createUuidV4(): string { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID(); } const bytes = new Uint8Array(16); if (typeof crypto !== "undefined" && crypto.getRandomValues) { crypto.getRandomValues(bytes); } else { for (let i = 0; i < bytes.length; i += 1) { bytes[i] = Math.floor(Math.random() * 256); } } bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")); return [ hex.slice(0, 4).join(""), hex.slice(4, 6).join(""), hex.slice(6, 8).join(""), hex.slice(8, 10).join(""), hex.slice(10, 16).join(""), ].join("-"); } /** * 全局单例 */ export const deviceIdentifier = new DeviceIdentifier();