Files
cozsweet-frontend-nextjs/src/utils/device_identifier.ts
T
admin 6e51cb7d16 Refactor: Remove original Dart references from comments across multiple files
- Updated comments in various components, schemas, and repositories to remove references to original Dart files.
- Ensured consistency in documentation while maintaining the context of the code.
2026-06-29 11:31:21 +08:00

98 lines
2.9 KiB
TypeScript

"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<string> | null = null;
/**
* 获取或生成设备 ID
*
* 优先级:内存缓存 → localStorage 缓存 → 非生产 UUID / 生产 fingerprintjs
* 并发安全:多个并发调用会共享同一个初始化 Promise
*/
async getDeviceId(): Promise<string> {
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<string> {
// 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();