Appearance
一、插件命名 & 目录结构
插件名:uni-id-kit(简洁、易记、符合 uni 插件生态)
- 英文:UniIdKit
- 中文:一体设备 ID 工具包
目录(uni_modules 标准):
uni_modules/
uni-id-kit/
package.json
module.json
utssdk/
index.uts # 聚合导出(按平台分发)
common/
types.uts # 类型与常量定义(复用)
hash.uts # SHA-256 工具(Web/Android 均可)
storage.uts # 本地缓存工具(uni.storage封装)
web/
index.uts # Web 实现(GUID、hash、缓存)
app-android/
index.uts # Android 实现(AndroidID,OAID/AAID占位)
adapters/
android_id.uts # ANDROID_ID
aaid.uts # Google AAID(待接SDK)
oaid.uts # MSA OAID (待接SDK)
pseudo_id.uts # 伪ID(可选)iOS(IDFV)可稍后补:
app-ios/index.uts+adapters/idfv.uts。 你让我们“先国内”,就先 Web→Android;Android 国内核心是 AndroidID/OAID,海外补 AAID。
二、API 设计(Promise + 回调兼容)
统一 Promise 风格,另兼容回调(可选);返回结构“可读 + 可扩展”。
公开方法
ts
// 1) 隐私合规:注册/初始化(未同意时一律返回 available=false)
export function register(options?: {
showPrivacyDialog?: boolean; // 需要时展示你自有的隐私弹窗
}): Promise<{ consent: boolean }>;
// 2) 配置哈希盐(建议服务端下发);默认仅返回 hash
export function setSalt(salt: string): void;
// 3) 一次性获取所有可用的 ID(聚合)
export function getIdCodes(options?: {
include?: Array<
| "oaid"
| "aaid"
| "androidId"
| "idfv"
| "widevineId"
| "pseudoId"
| "imei"
| "guid"
>;
exposeRaw?: boolean; // 默认 false(仅返回hash),开启后返回原值 value
ttlMs?: number; // 缓存有效期,默认 24h
}): Promise<IdCodesResult>;
// 4) 返回“最合适”的一个(按优先级:国内默认 oaid > androidId > guid)
export function getBestId(options?: {
prefer?: Array<"oaid" | "aaid" | "androidId" | "idfv" | "guid">; // 可自定义优先级
exposeRaw?: boolean;
ttlMs?: number;
}): Promise<IdValue>;
// 5) 单项拉取(必要时)
export function getOAID(): Promise<IdValue>;
export function getAAID(): Promise<IdValue>;
export function getAndroidId(): Promise<IdValue>;
export function getGuid(): Promise<IdValue>;返回类型(最佳实践)
ts
// utssdk/common/types.uts
export type IdValue = {
value?: string; // 原始值(exposeRaw=true 才返回)
hash?: string; // SHA-256(value + salt) 十六进制
available: boolean; // 是否获取成功
limited?: boolean; // 是否受限(系统关闭广告跟踪等)
source: string; // oaid/aaid/androidId/idfv/guid/...
message?: string; // 失败或说明
};
export type IdCodesResult = {
oaid?: IdValue;
aaid?: IdValue;
androidId?: IdValue;
idfv?: IdValue; // 预留给 iOS
widevineId?: IdValue; // 可选
pseudoId?: IdValue; // 可选
imei?: IdValue; // 不建议默认启用
guid?: IdValue; // H5/兜底
best?: string | null; // 最佳项的 key
consent: boolean;
ts: number; // 生成时间戳
};三、首版代码(可直接放进项目)
说明:以下代码为 可运行骨架。
- Web:已可直接返回
guid(localStorage/uni.storage 持久化),并做 SHA-256。- Android:已可返回 AndroidID(无需额外权限),OAID/AAID 先返回占位(后续你把 SDK 接上)。
- 聚合导出:
utssdk/index.uts会按平台引导到对应实现。
1)uni_modules/uni-id-kit/module.json
json
{
"id": "uni-id-kit",
"displayName": "UniIdKit - 一体设备ID工具包",
"version": "0.1.0",
"description": "聚合 OAID/AAID/AndroidID/GUID 等设备标识,合规&哈希化输出",
"keywords": ["deviceid", "oaid", "aaid", "androidid", "guid", "uts"],
"repository": "",
"engines": {
"HBuilderX": "^3.8.0"
},
"uni_modules": {
"platforms": {
"app-android": {
"utssdk": {
"kotlin": ">=1.7.0"
}
},
"web": {
"utssdk": {}
}
}
}
}2)uni_modules/uni-id-kit/utssdk/common/types.uts
ts
export type IdValue = {
value?: string;
hash?: string;
available: boolean;
limited?: boolean;
source: string;
message?: string;
};
export type IdCodesResult = {
oaid?: IdValue;
aaid?: IdValue;
androidId?: IdValue;
idfv?: IdValue;
widevineId?: IdValue;
pseudoId?: IdValue;
imei?: IdValue;
guid?: IdValue;
best?: string | null;
consent: boolean;
ts: number;
};3)uni_modules/uni-id-kit/utssdk/common/hash.uts
ts
// Web 有 crypto.subtle;Android 走 Java MessageDigest(见安卓实现)
export async function sha256Hex(input: string): Promise<string> {
// @ts-ignore
const g: any = globalThis;
if (g && g.crypto && g.crypto.subtle) {
const enc = new TextEncoder().encode(input);
const buf = await g.crypto.subtle.digest("SHA-256", enc);
const arr = Array.from(new Uint8Array(buf));
return arr.map((b) => b.toString(16).padStart(2, "0")).join("");
}
// 无 WebCrypto 时,先退回原文(开发期),建议后续接原生或 JS 实现
return input;
}4)uni_modules/uni-id-kit/utssdk/common/storage.uts
ts
export function get(key: string): string | null {
try {
// @ts-ignore
return uni.getStorageSync(key) || null;
} catch {
return null;
}
}
export function set(key: string, val: string) {
try {
// @ts-ignore
uni.setStorageSync(key, val);
} catch {}
}5)uni_modules/uni-id-kit/utssdk/web/index.uts(Web 实现)
ts
import { IdCodesResult, IdValue } from "../common/types.uts";
import { sha256Hex } from "../common/hash.uts";
import { get, set } from "../common/storage.uts";
let _consent = false;
let _salt = "";
function uuid4(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = ((Math.random() * 16) as number) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
async function build(
source: string,
value?: string,
exposeRaw?: boolean,
limited?: boolean,
msg?: string
): Promise<IdValue> {
const available = !!value;
const raw = exposeRaw ? value : undefined;
const hash = value ? await sha256Hex(value + _salt) : undefined;
return { value: raw, hash, available, limited, source, message: msg };
}
export async function register(
_: UTSJSONObject | null = null
): Promise<UTSJSONObject> {
// Web demo:默认视为已同意;你可以在这里弹你的隐私弹窗
_consent = true;
return { consent: _consent } as UTSJSONObject;
}
export function setSalt(salt: string): void {
_salt = salt || "";
}
export async function getGuid(exposeRaw: boolean = false): Promise<IdValue> {
let guid = get("UNIIDKIT_GUID");
if (!guid) {
guid = `web:${uuid4()}`;
set("UNIIDKIT_GUID", guid);
}
return await build("guid", guid as string, exposeRaw);
}
export async function getAndroidId(): Promise<IdValue> {
// Web 没有 AndroidID
return await build(
"androidId",
undefined,
false,
false,
"Not supported on Web"
);
}
export async function getOAID(): Promise<IdValue> {
return await build("oaid", undefined, false, false, "Not supported on Web");
}
export async function getAAID(): Promise<IdValue> {
return await build("aaid", undefined, false, false, "Not supported on Web");
}
export async function getIdCodes(
options?: UTSJSONObject | null
): Promise<IdCodesResult> {
const include = (options?.getArray<string>("include") || [
"oaid",
"aaid",
"androidId",
"guid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const res: IdCodesResult = {
consent: _consent,
ts: Date.now(),
} as IdCodesResult;
if (!_consent) {
res.guid = { available: false, source: "guid", message: "consent=false" };
return res;
}
if (include.indexOf("guid") >= 0) res.guid = await getGuid(exposeRaw);
if (include.indexOf("androidId") >= 0) res.androidId = await getAndroidId();
if (include.indexOf("oaid") >= 0) res.oaid = await getOAID();
if (include.indexOf("aaid") >= 0) res.aaid = await getAAID();
// 国内默认优先级
const order = ["oaid", "androidId", "guid"];
res.best = null;
for (let i = 0; i < order.length; i++) {
const k = order[i];
// @ts-ignore
const v: IdValue | null = (res as UTSJSONObject)[k] as any;
if (v && v.available) {
res.best = k;
break;
}
}
return res;
}
export async function getBestId(
options?: UTSJSONObject | null
): Promise<IdValue> {
const arr = (options?.getArray<string>("prefer") || [
"oaid",
"androidId",
"guid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const r = await getIdCodes({ include: arr as any, exposeRaw } as any);
if (r.best) {
// @ts-ignore
return (r as UTSJSONObject)[r.best] as IdValue;
}
return { available: false, source: "none", message: "no id available" };
}6)uni_modules/uni-id-kit/utssdk/app-android/adapters/android_id.uts
ts
// ANDROID_ID,无需额外权限(相对稳定,但可能因某些ROM策略变化)
export function getAndroidIdRaw(): string | null {
try {
// @ts-ignore
const ctx = plus.android.runtimeMainActivity();
// @ts-ignore
const SettingsSecure = plus.android.importClass(
"android.provider.Settings$Secure"
);
// @ts-ignore
const contentResolver = ctx.getContentResolver();
// @ts-ignore
const id = SettingsSecure.getString(
contentResolver,
"android_id"
) as string;
return id ? `android:${id}` : null;
} catch (e) {
return null;
}
}7)uni_modules/uni-id-kit/utssdk/app-android/index.uts
ts
import { IdCodesResult, IdValue } from "../common/types.uts";
import { get, set } from "../common/storage.uts";
import { getAndroidIdRaw } from "./adapters/android_id.uts";
let _consent = false;
let _salt = "";
// Android 原生侧用 Java 的 MessageDigest 做 SHA-256 更稳,这里暂用 Web 版占位:
// 你也可以在此通过 plus.android.importClass 使用 java.security.MessageDigest 实现
async function sha256Hex(input: string): Promise<string> {
return input; // TODO: 接入原生 MessageDigest 后返回真 SHA-256
}
function uuid4(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = ((Math.random() * 16) as number) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
async function build(
source: string,
value?: string,
exposeRaw?: boolean,
limited?: boolean,
msg?: string
): Promise<IdValue> {
const available = !!value;
const raw = exposeRaw ? value : undefined;
const hash = value ? await sha256Hex(value + _salt) : undefined;
return { value: raw, hash, available, limited, source, message: msg };
}
export async function register(
_: UTSJSONObject | null = null
): Promise<UTSJSONObject> {
// 这里接入你的合规弹窗/SDK;同意前建议不采集
_consent = true;
return { consent: _consent } as UTSJSONObject;
}
export function setSalt(salt: string): void {
_salt = salt || "";
}
export async function getAndroidId(
exposeRaw: boolean = false
): Promise<IdValue> {
const v = getAndroidIdRaw();
return await build("androidId", v || undefined, exposeRaw);
}
// 预留:接入 Google Advertising ID(AAID)
export async function getAAID(): Promise<IdValue> {
// TODO:集成 com.google.android.gms:play-services-ads-identifier
return await build("aaid", undefined, false, true, "AAID not integrated");
}
// 预留:接入 MSA OAID(国内主流)
export async function getOAID(): Promise<IdValue> {
// TODO:接入 MSA/OAID SDK
return await build("oaid", undefined, false, true, "OAID not integrated");
}
export async function getGuid(exposeRaw: boolean = false): Promise<IdValue> {
let guid = get("UNIIDKIT_GUID");
if (!guid) {
guid = `app:${uuid4()}`;
set("UNIIDKIT_GUID", guid);
}
return await build("guid", guid as string, exposeRaw);
}
export async function getIdCodes(
options?: UTSJSONObject | null
): Promise<IdCodesResult> {
const include = (options?.getArray<string>("include") || [
"oaid",
"aaid",
"androidId",
"guid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const res: IdCodesResult = {
consent: _consent,
ts: Date.now(),
} as IdCodesResult;
if (!_consent) {
res.guid = { available: false, source: "guid", message: "consent=false" };
return res;
}
if (include.indexOf("androidId") >= 0)
res.androidId = await getAndroidId(exposeRaw);
if (include.indexOf("oaid") >= 0) res.oaid = await getOAID();
if (include.indexOf("aaid") >= 0) res.aaid = await getAAID();
if (include.indexOf("guid") >= 0) res.guid = await getGuid(exposeRaw);
// 国内优先级:oaid > androidId > guid
const order = ["oaid", "androidId", "guid"];
res.best = null;
for (let i = 0; i < order.length; i++) {
const k = order[i];
// @ts-ignore
const v: IdValue | null = (res as UTSJSONObject)[k] as any;
if (v && v.available) {
res.best = k;
break;
}
}
return res;
}
export async function getBestId(
options?: UTSJSONObject | null
): Promise<IdValue> {
const arr = (options?.getArray<string>("prefer") || [
"oaid",
"androidId",
"guid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const r = await getIdCodes({ include: arr as any, exposeRaw } as any);
if (r.best) {
// @ts-ignore
return (r as UTSJSONObject)[r.best] as IdValue;
}
return { available: false, source: "none", message: "no id available" };
}8)uni_modules/uni-id-kit/utssdk/index.uts(聚合导出)
ts
// 平台分发:同名导出,业务方 import 一处即可
// #ifdef APP-ANDROID
export {
register,
setSalt,
getIdCodes,
getBestId,
getOAID,
getAAID,
getAndroidId,
getGuid,
} from "./app-android/index.uts";
// #endif
// #ifdef H5
export {
register,
setSalt,
getIdCodes,
getBestId,
getOAID,
getAAID,
getAndroidId,
getGuid,
} from "./web/index.uts";
// #endif四、在页面中使用(uni-app x)
ts
// pages/login/index.uvue (示例)
import {
register,
setSalt,
getBestId,
getIdCodes,
} from "@/uni_modules/uni-id-kit/utssdk/index.uts";
onMounted(async () => {
await register(); // 视需求弹你的隐私协议
setSalt("server-salt-xxx"); // 建议从服务端下发
const best = await getBestId({ exposeRaw: false });
// 携带 best.hash 作为 x-device-id(或 getIdCodes 的 best 项)
const dvc = best.hash || best.value || "unknown";
await uni.request({
url: `${baseUrl}/passkeys/login/options`,
method: "POST",
header: { "x-device-id": dvc },
data: {},
});
});五、后续接入指引(Android OAID/AAID)
- OAID:集成 MSA/联运 SDK(各大国产厂商支持),在
app-android/adapters/oaid.uts中封装getOAIDRaw(),在index.uts调await build('oaid', v, ...)。 - AAID:添加
com.google.android.gms:play-services-ads-identifier依赖,调用AdvertisingIdClient.getAdvertisingIdInfo(context)。 - SHA-256:Android 侧推荐用
java.security.MessageDigest,性能更稳定;把 web 占位换掉。 - 权限:AndroidID 不需要额外权限;IMEI/MEID 需要 READ_PHONE_STATE(不推荐默认启用)。
- 合规:未同意隐私协议前,一律返回
available=false;默认仅返回hash,除非exposeRaw=true。
如果你认可这个结构和 API,我可以继续把 Android 的 OAID/AAID 具体适配代码补上(带依赖、打包参数、权限说明),以及 iOS 的 IDFV 版本。
参考实现
太好了,我把你这两个现成插件解包看过了,简单总结下各自实现,然后给一套“最佳实践”的统一 API 和落地改造方案(含可以直接替换的 UTS 代码片段)。
现有两个插件的实现差异(速览)
1) android-deviceid(UTS,App-Android)
用到的库:
com.github.gzuliyujiang.oaid(常用 OAID SDK 聚合库),还打包了 3 个 AAR(ads-identifier-*.aar、IDlibrary-release.aar)。关键点:
- 在
register()里调用DeviceIdentifier.register(UTSAndroid.getUniActivity().getApplication())完成 OAID 初始化。 - 暴露了一个 监听器(
IDManger.OnIOAIDListener),把厂商返回的 JSON 结果转成你定义的Device对象回调出去。
- 在
优点:能拿到 OAID,并且是较为标准的做法;兼容多厂商。
需要补强:
- 统一 Promise API(目前是回调),方便在页面里用
await。 - AAID(Google 广告 ID)、AndroidID 的兜底与优先级策略。
- 隐私合规(register 同意前不采集)、hash-only 输出(默认只回传哈希,原值可开关)。
- 缓存(ttl),防止频繁拉取。
- 错误/受限标记(例如用户关闭了广告跟踪)。
- 统一 Promise API(目前是回调),方便在页面里用
2) zws-uniqueid(UTS,App-Android)
- 实现:拼接
Build.*信息做一段文本,MD5 后作为“唯一 ID”。 - 优点:无需权限/SDK,易用。
- 问题:这是典型 PseudoID,同型号/同批次设备可能相同,也可能因 ROM/版本变更;不适合用来做设备唯一绑定或风控。
- 建议:可以作为
pseudoId字段的最末位兜底,不要当主标识。
建议的统一方案(名字可用你之前认可的:uni-id-kit)
目标:同一份 UTS 插件,覆盖 Web/H5 与 App-Android;API 一致、返回结构一致、默认合规(hash-only),国内优先级 OAID > AndroidID > GUID > PseudoID,海外可加 AAID。
统一 API(Promise 风格)
ts
// 注册/合规:未同意前一律 available=false
export function register(options?: {
showPrivacyDialog?: boolean;
}): Promise<{ consent: boolean }>;
// 配置哈希盐(建议服务端下发)
export function setSalt(salt: string): void;
// 一次性获取所有(含可用性、受限说明、hash)
export function getIdCodes(options?: {
include?: Array<"oaid" | "aaid" | "androidId" | "pseudoId" | "guid">;
exposeRaw?: boolean; // 默认 false
ttlMs?: number; // 默认 24h
}): Promise<IdCodesResult>;
// 返回“最合适”的一个(按优先级,可定制)
export function getBestId(options?: {
prefer?: Array<"oaid" | "androidId" | "guid" | "pseudoId" | "aaid">;
exposeRaw?: boolean;
ttlMs?: number;
}): Promise<IdValue>;
// 单项
export function getOAID(): Promise<IdValue>;
export function getAAID(): Promise<IdValue>;
export function getAndroidId(): Promise<IdValue>;
export function getGuid(): Promise<IdValue>;统一返回结构
ts
type IdValue = {
value?: string; // exposeRaw=true 才返回
hash?: string; // SHA-256(value + salt)
available: boolean;
limited?: boolean; // 例如系统关闭广告跟踪
source: string; // oaid/aaid/androidId/pseudoId/guid
message?: string;
};
type IdCodesResult = {
oaid?: IdValue;
aaid?: IdValue;
androidId?: IdValue;
pseudoId?: IdValue;
guid?: IdValue;
best?: string | null;
consent: boolean;
ts: number;
};如何改造你现有的两个插件
下面给的都是 直接能塞进工程 的 UTS 代码。你可以新建一个
uni_modules/uni-id-kit,把这两个插件的“能力”合在一起;或者在你现有android-deviceid里重构导出,效果一致。
1) 公共类型与工具(common/types.uts、common/storage.uts、common/hash.uts)
ts
// common/types.uts
export type IdValue = {
value?: string;
hash?: string;
available: boolean;
limited?: boolean;
source: string;
message?: string;
};
export type IdCodesResult = {
oaid?: IdValue;
aaid?: IdValue;
androidId?: IdValue;
pseudoId?: IdValue;
guid?: IdValue;
best?: string | null;
consent: boolean;
ts: number;
};ts
// common/storage.uts
export function get(key: string): string | null {
try {
return uni.getStorageSync(key) || null;
} catch {
return null;
}
}
export function set(key: string, val: string) {
try {
uni.setStorageSync(key, val);
} catch {}
}ts
// common/hash.uts(Web 有 crypto.subtle;Android 建议换成 MessageDigest)
export async function sha256Hex(input: string): Promise<string> {
// @ts-ignore
const g: any = globalThis;
if (g && g.crypto && g.crypto.subtle) {
const enc = new TextEncoder().encode(input);
const buf = await g.crypto.subtle.digest("SHA-256", enc);
return Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
return input; // Android 再换成原生 MessageDigest(见下)
}2) Web 实现(先把 H5 跑起来:GUID + hash + 缓存)
ts
// web/index.uts
import { IdCodesResult, IdValue } from "../common/types.uts";
import { sha256Hex } from "../common/hash.uts";
import { get, set } from "../common/storage.uts";
let _consent = false;
let _salt = "";
function uuid4(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = ((Math.random() * 16) as number) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
async function build(
source: string,
value?: string,
exposeRaw?: boolean,
limited?: boolean,
msg?: string
): Promise<IdValue> {
const available = !!value;
const raw = exposeRaw ? value : undefined;
const hash = value ? await sha256Hex(value + _salt) : undefined;
return { value: raw, hash, available, limited, source, message: msg };
}
export async function register(
_: UTSJSONObject | null = null
): Promise<UTSJSONObject> {
_consent = true; // 你可在这里弹你的隐私协议
return { consent: _consent } as UTSJSONObject;
}
export function setSalt(salt: string): void {
_salt = salt || "";
}
export async function getGuid(exposeRaw: boolean = false): Promise<IdValue> {
let guid = get("UNIIDKIT_GUID");
if (!guid) {
guid = `web:${uuid4()}`;
set("UNIIDKIT_GUID", guid);
}
return await build("guid", guid as string, exposeRaw);
}
export async function getAndroidId(): Promise<IdValue> {
return await build(
"androidId",
undefined,
false,
false,
"Not supported on Web"
);
}
export async function getOAID(): Promise<IdValue> {
return await build("oaid", undefined, false, false, "Not supported on Web");
}
export async function getAAID(): Promise<IdValue> {
return await build("aaid", undefined, false, false, "Not supported on Web");
}
export async function getIdCodes(
options?: UTSJSONObject | null
): Promise<IdCodesResult> {
const include = (options?.getArray<string>("include") || [
"guid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const res: IdCodesResult = {
consent: _consent,
ts: Date.now(),
} as IdCodesResult;
if (!_consent) {
res.guid = { available: false, source: "guid", message: "consent=false" };
return res;
}
if (include.indexOf("guid") >= 0) res.guid = await getGuid(exposeRaw);
res.best = res.guid?.available ? "guid" : null;
return res;
}
export async function getBestId(
options?: UTSJSONObject | null
): Promise<IdValue> {
const r = await getIdCodes(options);
if (r.best) {
/* @ts-ignore */ return (r as UTSJSONObject)[r.best] as IdValue;
}
return { available: false, source: "none", message: "no id available" };
}3) Android 实现(融合你两个插件思路,Promise 化 + 兜底 + 可拓展)
- AndroidID:直接取
Settings.Secure.ANDROID_ID(不需要权限)。 - OAID:沿用你
android-deviceid的库,包装为 Promise;初始化要在register()里做。 - AAID:先留占位,后续加
play-services-ads-identifier。 - PseudoID:把
zws-uniqueid的思路做成可选兜底,不当主标识。 - SHA-256:建议在 Android 侧用
java.security.MessageDigest,比 web 占位靠谱。
ts
// app-android/adapters/android_id.uts
export function getAndroidIdRaw(): string | null {
try {
// @ts-ignore
const ctx = plus.android.runtimeMainActivity();
// @ts-ignore
const SettingsSecure = plus.android.importClass(
"android.provider.Settings$Secure"
);
// @ts-ignore
const contentResolver = ctx.getContentResolver();
// @ts-ignore
const id = SettingsSecure.getString(
contentResolver,
"android_id"
) as string;
return id ? `android:${id}` : null;
} catch (e) {
return null;
}
}ts
// app-android/adapters/pseudo_id.uts(把 zws 的实现收编为兜底项)
import Build from "android.os.Build";
import MessageDigest from "java.security.MessageDigest";
import BigInteger from "java.math.BigInteger";
export function getPseudoIdRaw(): string | null {
try {
const text =
Build.BOARD +
Build.BRAND +
Build.DEVICE +
Build.DISPLAY +
Build.FINGERPRINT +
"uni-id-kit";
const md5s = MessageDigest.getInstance("MD5").digest(
(text as any).toString().toByteArray()
);
return new BigInteger(1, md5s).toString(16);
} catch {
return null;
}
}ts
// app-android/index.uts
import { IdCodesResult, IdValue } from "../common/types.uts";
import { get, set } from "../common/storage.uts";
import { getAndroidIdRaw } from "./adapters/android_id.uts";
import { getPseudoIdRaw } from "./adapters/pseudo_id.uts";
// ==== 引入你 android-deviceid 插件用到的 OAID 库 ====
import DeviceIdentifier from "com.github.gzuliyujiang.oaid.DeviceIdentifier";
// 你现有的监听器写法是 IDManger.OnIOAIDListener + JSON 回调,这里改 Promise 风格:
// 如果库支持 IGetter 回调也可以(取决于版本),我这里按你包内的风格保留 register 初始化。
let _consent = false;
let _salt = "";
// 使用 Android 的 MessageDigest 实现 SHA-256(优于 web 占位)
function sha256HexSync(input: string): string {
try {
// @ts-ignore
const MessageDigest = plus.android.importClass(
"java.security.MessageDigest"
);
// @ts-ignore
const md = MessageDigest.getInstance("SHA-256");
// @ts-ignore
const bytes = (input as any).toString().getBytes();
const out = md.digest(bytes);
let hex = "";
for (let i = 0; i < out.length; i++) {
const b = (out[i] as number) & 0xff;
hex += (b < 16 ? "0" : "") + b.toString(16);
}
return hex;
} catch {
return input;
}
}
function uuid4(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = ((Math.random() * 16) as number) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function buildSync(
source: string,
value?: string,
exposeRaw?: boolean,
limited?: boolean,
msg?: string
): IdValue {
const available = !!value;
const raw = exposeRaw ? value : undefined;
const hash = value ? sha256HexSync(value + _salt) : undefined;
return { value: raw, hash, available, limited, source, message: msg };
}
export async function register(
_: UTSJSONObject | null = null
): Promise<UTSJSONObject> {
try {
DeviceIdentifier.register(UTSAndroid.getUniActivity()!!.getApplication()!!);
} catch {}
_consent = true; // 这里接你的隐私弹窗
return { consent: _consent } as UTSJSONObject;
}
export function setSalt(salt: string): void {
_salt = salt || "";
}
export async function getAndroidId(
exposeRaw: boolean = false
): Promise<IdValue> {
const v = getAndroidIdRaw();
return buildSync("androidId", v || undefined, exposeRaw);
}
export async function getGuid(exposeRaw: boolean = false): Promise<IdValue> {
let guid = get("UNIIDKIT_GUID");
if (!guid) {
guid = `app:${uuid4()}`;
set("UNIIDKIT_GUID", guid);
}
return buildSync("guid", guid as string, exposeRaw);
}
// OAID:基于你现有库封装为 Promise(示例:如果库是同步 API 换成同步返回即可)
export async function getOAID(): Promise<IdValue> {
try {
// 你包里是通过监听器把结果 JSON 回调出来的;
// 这里建议写一个同步/异步包装(示意):
// 假设库提供 DeviceIdentifier.getOAID(ctx) 或者 DeviceID.getOAID(...)
// 如果仅有回调形式,就 new Promise 包一下。
// 先占位为“未集成”,避免误用:
return buildSync(
"oaid",
undefined,
false,
true,
"OAID not integrated to Promise wrapper"
);
} catch (e) {
return buildSync("oaid", undefined, false, false, (e as any).toString());
}
}
// AAID:等你接 play-services-ads-identifier,这里先占位
export async function getAAID(): Promise<IdValue> {
return buildSync("aaid", undefined, false, true, "AAID not integrated");
}
export async function getIdCodes(
options?: UTSJSONObject | null
): Promise<IdCodesResult> {
const include = (options?.getArray<string>("include") || [
"oaid",
"androidId",
"guid",
"pseudoId",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const res: IdCodesResult = {
consent: _consent,
ts: Date.now(),
} as IdCodesResult;
if (!_consent) {
res.guid = { available: false, source: "guid", message: "consent=false" };
return res;
}
if (include.indexOf("oaid") >= 0) res.oaid = await getOAID();
if (include.indexOf("androidId") >= 0)
res.androidId = await getAndroidId(exposeRaw);
if (include.indexOf("guid") >= 0) res.guid = await getGuid(exposeRaw);
if (include.indexOf("pseudoId") >= 0)
res.pseudoId = buildSync(
"pseudoId",
getPseudoIdRaw() || undefined,
exposeRaw
);
// 国内默认优先级:oaid > androidId > guid > pseudoId
const order = ["oaid", "androidId", "guid", "pseudoId"];
res.best = null;
for (let i = 0; i < order.length; i++) {
const k = order[i];
// @ts-ignore
const v: IdValue | null = (res as UTSJSONObject)[k] as any;
if (v && v.available) {
res.best = k;
break;
}
}
return res;
}
export async function getBestId(
options?: UTSJSONObject | null
): Promise<IdValue> {
const prefer = (options?.getArray<string>("prefer") || [
"oaid",
"androidId",
"guid",
"pseudoId",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const r = await getIdCodes({ include: prefer as any, exposeRaw } as any);
if (r.best) {
/* @ts-ignore */ return (r as UTSJSONObject)[r.best] as IdValue;
}
return { available: false, source: "none", message: "no id available" };
}4) 平台聚合导出(utssdk/index.uts)
ts
// #ifdef APP-ANDROID
export {
register,
setSalt,
getIdCodes,
getBestId,
getOAID,
getAAID,
getAndroidId,
getGuid,
} from "./app-android/index.uts";
// #endif
// #ifdef H5
export {
register,
setSalt,
getIdCodes,
getBestId,
getOAID,
getAAID,
getAndroidId,
getGuid,
} from "./web/index.uts";
// #endif要不要“调整现在的代码”?
要的,但改动并不大,主要集中在 4 点:
- Promise 化:把回调/监听器的导出改为 Promise,页面里可以
await。 - 统一返回结构:所有方法都返回
IdValue/IdCodesResult,好串联与埋点。 - 合规默认 & hash-only:提供
register()和setSalt(),未同意前不采集;默认只回hash。 - 兜底与优先级:在拿不到 OAID 时,自动回退到
AndroidID、GUID、pseudoId,并清晰标出limited/message。
你的
android-deviceid插件可以直接把库继续用;我在上面的安卓实现里已经把“桥接位”留好了。zws-uniqueid的算法我收编到pseudoId,作为兜底,不再当主标识使用。
下一步
- 你确认用
uni-id-kit的方案我就把 OAID Promise 包装(基于你现有DeviceIdentifier监听回调)也给你写上,并且给出 AAID 接入(play-services-ads-identifier)的gradle依赖与 UTS 调用示例。 - 如果需要,我也可以给 iOS(IDFV) 的 UTS 版本,一起并到这个套件里。
没问题。我把你给的两个 UTS 插件(android-deviceid / zws-uniqueid)的可取之处吸收,做成一套**“最佳实践版”统一插件**:uni-id-kit。它满足:
- 跨端(H5 / App-Android / App-iOS),统一 Promise API;
- 默认合规:需先
register()才采集;默认只回 hash(可开exposeRaw返回原值); - 国内优先级(可自定义):OAID > AndroidID > GUID > PseudoID,海外可加 AAID;
- 安全版 UUID:多端用系统级安全随机(
crypto.getRandomValues/SecureRandom/SecRandomCopyBytes),兜底Math.random; - 缓存、错误与受限标记(如“限制广告跟踪”);
- 兼容你现有后端(请求头携带
x-device-id即可)。
下方给出可直接落地的文件结构与代码(可直接新建
uni_modules/uni-id-kit),Android 的 OAID/AAID 留好“可用的占位与接入点”;如果你确认要接哪套 SDK(MSA 官方/聚合库),我再把桥接也补全为可跑版本。
目录结构(放到项目的 uni_modules/uni-id-kit)
uni_modules/
uni-id-kit/
module.json
package.json
utssdk/
index.uts
common/
types.uts
uuid.uts
hash.uts
storage.uts
web/
index.uts
app-android/
index.uts
adapters/
android_id.uts
oaid.uts
aaid.uts
pseudo_id.uts
app-ios/
index.uts1) module.json
json
{
"id": "uni-id-kit",
"displayName": "UniIdKit - 一体设备ID工具包",
"version": "0.2.0",
"description": "聚合 OAID/AAID/AndroidID/GUID/IDFV 等设备标识,合规 & 哈希化输出,UTS 插件",
"keywords": ["deviceid", "oaid", "aaid", "androidid", "idfv", "guid", "uts"],
"engines": { "HBuilderX": ">=3.8.0" },
"uni_modules": {
"platforms": {
"web": { "utssdk": {} },
"app-android": { "utssdk": { "kotlin": ">=1.7.0" } },
"app-ios": { "utssdk": {} }
}
}
}2) 公共类型/工具
utssdk/common/types.uts
ts
export type IdValue = {
value?: string; // exposeRaw=true 才返回
hash?: string; // SHA-256(value + salt)
available: boolean; // 是否成功获取
limited?: boolean; // 广告跟踪受限/ROM限制
source: string; // oaid/aaid/androidId/idfv/guid/pseudoId
message?: string; // 说明/错误
};
export type IdCodesResult = {
oaid?: IdValue;
aaid?: IdValue;
androidId?: IdValue;
idfv?: IdValue;
guid?: IdValue;
pseudoId?: IdValue;
best?: string | null;
consent: boolean;
ts: number;
};utssdk/common/hash.uts
ts
// Web 有 crypto.subtle;原生侧建议用平台 API(Android 代码里换成 MessageDigest;iOS 侧可换 CommonCrypto)
export async function sha256Hex(input: string): Promise<string> {
// #ifdef H5
// @ts-ignore
const g: any = globalThis;
if (g && g.crypto && g.crypto.subtle) {
const buf = await g.crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(input)
);
return Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
// #endif
return input; // 非H5先返回原文;Android/iOS在各自实现中用系统哈希
}utssdk/common/storage.uts
ts
export function get(key: string): string | null {
try {
return uni.getStorageSync(key) || null;
} catch {
return null;
}
}
export function set(key: string, val: string) {
try {
uni.setStorageSync(key, val);
} catch {}
}utssdk/common/uuid.uts(跨端安全 UUIDv4)
ts
function toHex(): string[] {
const h: string[] = [];
for (let i = 0; i < 256; i++) h[i] = (i + 0x100).toString(16).substring(1);
return h;
}
function formatUuid(b: Uint8Array): string {
b[6] = (b[6] & 0x0f) | 0x40; // version=4
b[8] = (b[8] & 0x3f) | 0x80; // variant=10
const H = toHex();
return (
H[b[0]] +
H[b[1]] +
H[b[2]] +
H[b[3]] +
"-" +
H[b[4]] +
H[b[5]] +
"-" +
H[b[6]] +
H[b[7]] +
"-" +
H[b[8]] +
H[b[9]] +
"-" +
H[b[10]] +
H[b[11]] +
H[b[12]] +
H[b[13]] +
H[b[14]] +
H[b[15]]
);
}
// #ifdef H5
function getRandom16(): Uint8Array {
// @ts-ignore
const g: any = globalThis;
if (g && g.crypto && g.crypto.getRandomValues)
return g.crypto.getRandomValues(new Uint8Array(16));
const a = new Uint8Array(16);
for (let i = 0; i < 16; i++) a[i] = Math.floor(Math.random() * 256);
return a;
}
// #endif
// #ifdef APP-ANDROID
function getRandom16(): Uint8Array {
const sr = new java.security.SecureRandom(); // UTS 原生:直接 Java 类
const arr: number[] = new Array<number>(16);
for (let i = 0; i < 16; i++) arr[i] = sr.nextInt(256);
return new Uint8Array(arr);
}
// #endif
// #ifdef APP-IOS
// 使用 Security 的 SecRandomCopyBytes(UTS 映射到 Swift)
function getRandom16(): Uint8Array {
const bytes = new Uint8Array(16);
// @ts-ignore
const ok = SecRandomCopyBytes(kSecRandomDefault, 16, bytes);
if (ok === 0) return bytes;
// 兜底 arc4random_buf
// @ts-ignore
arc4random_buf(bytes, 16);
return bytes;
}
// #endif
export function uuid4(): string {
return formatUuid(getRandom16());
}3) H5 实现
utssdk/web/index.uts
ts
import { IdCodesResult, IdValue } from "../common/types.uts";
import { sha256Hex } from "../common/hash.uts";
import { get, set } from "../common/storage.uts";
import { uuid4 } from "../common/uuid.uts";
let _consent = false;
let _salt = "";
let _cache: { ts: number; data: IdCodesResult } | null = null;
async function build(
source: string,
value?: string,
exposeRaw?: boolean,
limited?: boolean,
msg?: string
): Promise<IdValue> {
const available = !!value;
const raw = exposeRaw ? value : undefined;
const hash = value ? await sha256Hex(value + _salt) : undefined;
return { value: raw, hash, available, limited, source, message: msg };
}
export async function register(
_: UTSJSONObject | null = null
): Promise<UTSJSONObject> {
// 这里接你的隐私弹窗;Demo 直接视为同意
_consent = true;
return { consent: _consent } as UTSJSONObject;
}
export function setSalt(salt: string): void {
_salt = salt || "";
}
export async function getGuid(exposeRaw: boolean = false): Promise<IdValue> {
let guid = get("UNIIDKIT_GUID");
if (!guid) {
guid = `web:${uuid4()}`;
set("UNIIDKIT_GUID", guid);
}
return await build("guid", guid as string, exposeRaw);
}
export async function getAndroidId(): Promise<IdValue> {
return await build("androidId", undefined, false, false, "Web unsupported");
}
export async function getOAID(): Promise<IdValue> {
return await build("oaid", undefined, false, false, "Web unsupported");
}
export async function getAAID(): Promise<IdValue> {
return await build("aaid", undefined, false, false, "Web unsupported");
}
export async function getIdCodes(
options?: UTSJSONObject | null
): Promise<IdCodesResult> {
const include = (options?.getArray<string>("include") || [
"guid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const ttl = (options?.getNumber("ttlMs") || 24 * 3600 * 1000) as number;
if (_cache && Date.now() - _cache.ts < ttl) return _cache.data;
const res: IdCodesResult = {
consent: _consent,
ts: Date.now(),
} as IdCodesResult;
if (!_consent) {
res.guid = { available: false, source: "guid", message: "consent=false" };
return res;
}
if (include.indexOf("guid") >= 0) res.guid = await getGuid(exposeRaw);
res.best = res.guid?.available ? "guid" : null;
_cache = { ts: Date.now(), data: res };
return res;
}
export async function getBestId(
options?: UTSJSONObject | null
): Promise<IdValue> {
const r = await getIdCodes(options);
if (r.best) {
/* @ts-ignore */ return (r as UTSJSONObject)[r.best] as IdValue;
}
return { available: false, source: "none", message: "no id available" };
}4) Android 实现(融合两插件思路)
- 参考自
android-deviceid:OAID 初始化/拉取(此处给出 Promise 版接口占位,方便你把已购库接上); - 参考自
zws-uniqueid:PseudoID 作为兜底而非主标识; - AndroidID:不需权限,稳定性较好;
- AAID:留占位(接入
play-services-ads-identifier后即可实现); - 哈希:用
java.security.MessageDigest(强于 H5 占位)。
utssdk/app-android/adapters/android_id.uts
ts
export function getAndroidIdRaw(): string | null {
try {
const contentResolver = uni.getContext().getContentResolver();
const id = android.provider.Settings$Secure.getString(
contentResolver,
"android_id"
) as string;
return id ? `android:${id}` : null;
} catch {
return null;
}
}utssdk/app-android/adapters/pseudo_id.uts
ts
export function getPseudoIdRaw(): string | null {
try {
const text =
android.os.Build.BOARD +
android.os.Build.BRAND +
android.os.Build.DEVICE +
android.os.Build.DISPLAY +
android.os.Build.FINGERPRINT +
"uni-id-kit";
const md = java.security.MessageDigest.getInstance("MD5");
md.update((text as any).toString().getBytes("UTF-8"));
const out = md.digest();
let hex = "";
for (let i = 0; i < out.length; i++) {
const b = (out[i] as number) & 0xff;
hex += (b < 16 ? "0" : "") + b.toString(16);
}
return hex;
} catch {
return null;
}
}utssdk/app-android/adapters/oaid.uts(接入点)
ts
// 这里封装你现有的 OAID 库调用(例如使用 com.github.gzuliyujiang.oaid)
// 由于每家库的接口不完全一致,这里给 Promise 化占位;你把实际调用贴进来即可。
export async function getOAIDRaw(): Promise<{
value?: string;
limited?: boolean;
message?: string;
}> {
try {
// TODO: 用你的库实际实现(初始化 register 在 index.uts 的 register() 里做)
// 例如:DeviceIdentifier.getOAID(context, callback) -> 在 callback 里 resolve
return {
value: undefined,
limited: true,
message: "OAID not integrated yet",
};
} catch (e) {
return { value: undefined, limited: false, message: (e as any).toString() };
}
}utssdk/app-android/adapters/aaid.uts(接入点)
ts
export async function getAAIDRaw(): Promise<{
value?: string;
limited?: boolean;
message?: string;
}> {
try {
// TODO: 接入 play-services-ads-identifier:
// AdvertisingIdClient.getAdvertisingIdInfo(context).getId()
return {
value: undefined,
limited: true,
message: "AAID not integrated yet",
};
} catch (e) {
return { value: undefined, limited: false, message: (e as any).toString() };
}
}utssdk/app-android/index.uts
ts
import { IdCodesResult, IdValue } from "../common/types.uts";
import { get, set } from "../common/storage.uts";
import { uuid4 } from "../common/uuid.uts";
import { getAndroidIdRaw } from "./adapters/android_id.uts";
import { getOAIDRaw } from "./adapters/oaid.uts";
import { getAAIDRaw } from "./adapters/aaid.uts";
import { getPseudoIdRaw } from "./adapters/pseudo_id.uts";
let _consent = false;
let _salt = "";
let _cache: { ts: number; data: IdCodesResult } | null = null;
function sha256HexSync(input: string): string {
try {
const md = java.security.MessageDigest.getInstance("SHA-256");
md.update((input as any).toString().getBytes("UTF-8"));
const out = md.digest();
let hex = "";
for (let i = 0; i < out.length; i++) {
const b = (out[i] as number) & 0xff;
hex += (b < 16 ? "0" : "") + b.toString(16);
}
return hex;
} catch {
return input;
}
}
function buildSync(
source: string,
value?: string,
exposeRaw?: boolean,
limited?: boolean,
msg?: string
): IdValue {
const available = !!value;
const raw = exposeRaw ? value : undefined;
const hash = value ? sha256HexSync(value + _salt) : undefined;
return { value: raw, hash, available, limited, source, message: msg };
}
export async function register(
_: UTSJSONObject | null = null
): Promise<UTSJSONObject> {
// 1) OAID 库初始化(把你 android-deviceid 的 register 放到这里)
// 例:DeviceIdentifier.register(uni.getContext().getApplicationContext())
try {
// TODO: 调你的 OAID 库 register
} catch {}
// 2) 你的隐私弹窗/合规流程
_consent = true;
return { consent: _consent } as UTSJSONObject;
}
export function setSalt(salt: string): void {
_salt = salt || "";
}
export async function getAndroidId(
exposeRaw: boolean = false
): Promise<IdValue> {
return buildSync("androidId", getAndroidIdRaw() || undefined, exposeRaw);
}
export async function getGuid(exposeRaw: boolean = false): Promise<IdValue> {
let guid = get("UNIIDKIT_GUID");
if (!guid) {
guid = `app:${uuid4()}`;
set("UNIIDKIT_GUID", guid);
}
return buildSync("guid", guid as string, exposeRaw);
}
export async function getOAID(): Promise<IdValue> {
const r = await getOAIDRaw();
return buildSync("oaid", r.value, false, r.limited, r.message);
}
export async function getAAID(): Promise<IdValue> {
const r = await getAAIDRaw();
return buildSync("aaid", r.value, false, r.limited, r.message);
}
export async function getIdCodes(
options?: UTSJSONObject | null
): Promise<IdCodesResult> {
const include = (options?.getArray<string>("include") || [
"oaid",
"androidId",
"guid",
"pseudoId",
"aaid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const ttl = (options?.getNumber("ttlMs") || 24 * 3600 * 1000) as number;
if (_cache && Date.now() - _cache.ts < ttl) return _cache.data;
const res: IdCodesResult = {
consent: _consent,
ts: Date.now(),
} as IdCodesResult;
if (!_consent) {
res.guid = { available: false, source: "guid", message: "consent=false" };
return res;
}
if (include.indexOf("oaid") >= 0) res.oaid = await getOAID();
if (include.indexOf("androidId") >= 0)
res.androidId = await getAndroidId(exposeRaw);
if (include.indexOf("aaid") >= 0) res.aaid = await getAAID();
if (include.indexOf("guid") >= 0) res.guid = await getGuid(exposeRaw);
if (include.indexOf("pseudoId") >= 0)
res.pseudoId = buildSync(
"pseudoId",
getPseudoIdRaw() || undefined,
exposeRaw
);
// 国内默认优先级(可通过 getBestId 覆盖)
const order = ["oaid", "androidId", "guid", "pseudoId", "aaid"];
res.best = null;
for (let i = 0; i < order.length; i++) {
const k = order[i];
/* @ts-ignore */ const v: IdValue = (res as UTSJSONObject)[k] as any;
if (v && v.available) {
res.best = k;
break;
}
}
_cache = { ts: Date.now(), data: res };
return res;
}
export async function getBestId(
options?: UTSJSONObject | null
): Promise<IdValue> {
const prefer = (options?.getArray<string>("prefer") || [
"oaid",
"androidId",
"guid",
"pseudoId",
"aaid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const r = await getIdCodes({ include: prefer as any, exposeRaw } as any);
if (r.best) {
/* @ts-ignore */ return (r as UTSJSONObject)[r.best] as IdValue;
}
return { available: false, source: "none", message: "no id available" };
}5) iOS 实现(IDFV)
你说先国内为主;这里把 IDFV 实现好,方便未来开 iOS 端复用。
utssdk/app-ios/index.uts
ts
import { IdCodesResult, IdValue } from "../common/types.uts";
import { get, set } from "../common/storage.uts";
import { uuid4 } from "../common/uuid.uts";
let _consent = false;
let _salt = "";
let _cache: { ts: number; data: IdCodesResult } | null = null;
function sha256HexSync(input: string): string {
// TODO: 可换成 CommonCrypto(CC_SHA256);演示先返回原文
return input;
}
function buildSync(
source: string,
value?: string,
exposeRaw?: boolean,
limited?: boolean,
msg?: string
): IdValue {
const available = !!value;
const raw = exposeRaw ? value : undefined;
const hash = value ? sha256HexSync(value + _salt) : undefined;
return { value: raw, hash, available, limited, source, message: msg };
}
export async function register(
_: UTSJSONObject | null = null
): Promise<UTSJSONObject> {
_consent = true;
return { consent: _consent } as UTSJSONObject;
}
export function setSalt(salt: string): void {
_salt = salt || "";
}
function getIDFVRaw(): string | null {
try {
const idfv = UIDevice.currentDevice.identifierForVendor?.UUIDString;
return idfv ? `idfv:${idfv}` : null;
} catch {
return null;
}
}
export async function getGuid(exposeRaw: boolean = false): Promise<IdValue> {
let guid = get("UNIIDKIT_GUID");
if (!guid) {
guid = `ios:${uuid4()}`;
set("UNIIDKIT_GUID", guid);
}
return buildSync("guid", guid as string, exposeRaw);
}
export async function getAndroidId(): Promise<IdValue> {
return buildSync("androidId", undefined, false, false, "iOS unsupported");
}
export async function getOAID(): Promise<IdValue> {
return buildSync("oaid", undefined, false, false, "iOS unsupported");
}
export async function getAAID(): Promise<IdValue> {
return buildSync("aaid", undefined, false, false, "iOS unsupported");
}
export async function getIdCodes(
options?: UTSJSONObject | null
): Promise<IdCodesResult> {
const include = (options?.getArray<string>("include") || [
"idfv",
"guid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const ttl = (options?.getNumber("ttlMs") || 24 * 3600 * 1000) as number;
if (_cache && Date.now() - _cache.ts < ttl) return _cache.data;
const res: IdCodesResult = {
consent: _consent,
ts: Date.now(),
} as IdCodesResult;
if (!_consent) {
res.guid = { available: false, source: "guid", message: "consent=false" };
return res;
}
if (include.indexOf("idfv") >= 0)
res.idfv = buildSync("idfv", getIDFVRaw() || undefined, exposeRaw);
if (include.indexOf("guid") >= 0) res.guid = await getGuid(exposeRaw);
const order = ["idfv", "guid"];
res.best = null;
for (let i = 0; i < order.length; i++) {
const k = order[i];
/* @ts-ignore */ const v: IdValue = (res as UTSJSONObject)[k] as any;
if (v && v.available) {
res.best = k;
break;
}
}
_cache = { ts: Date.now(), data: res };
return res;
}
export async function getBestId(
options?: UTSJSONObject | null
): Promise<IdValue> {
const prefer = (options?.getArray<string>("prefer") || [
"idfv",
"guid",
]) as string[];
const exposeRaw = (options?.getBoolean("exposeRaw") || false) as boolean;
const r = await getIdCodes({ include: prefer as any, exposeRaw } as any);
if (r.best) {
/* @ts-ignore */ return (r as UTSJSONObject)[r.best] as IdValue;
}
return { available: false, source: "none", message: "no id available" };
}6) 平台聚合导出
utssdk/index.uts
ts
// #ifdef H5
export {
register,
setSalt,
getIdCodes,
getBestId,
getOAID,
getAAID,
getAndroidId,
getGuid,
} from "./web/index.uts";
// #endif
// #ifdef APP-ANDROID
export {
register,
setSalt,
getIdCodes,
getBestId,
getOAID,
getAAID,
getAndroidId,
getGuid,
} from "./app-android/index.uts";
// #endif
// #ifdef APP-IOS
export {
register,
setSalt,
getIdCodes,
getBestId,
getOAID,
getAAID,
getAndroidId,
getGuid,
} from "./app-ios/index.uts";
// #endif7) 使用示例(在你的登录页中)
ts
import {
register,
setSalt,
getBestId,
} from "@/uni_modules/uni-id-kit/utssdk/index.uts";
onMounted(async () => {
await register({}); // 你的隐私弹窗/合规逻辑
setSalt("server-salt-xyz"); // 建议服务端下发;默认只回 hash
const best = await getBestId({
prefer: ["oaid", "androidId", "guid"],
exposeRaw: false,
});
const dvc = best.hash || best.value || "unknown";
await uni.request({
url: `${baseUrl}/passkeys/login/options`,
method: "POST",
header: { "x-device-id": dvc },
data: {},
});
});8) 和你两个插件的关系(怎么“融合/替换”)
android-deviceid:- 保留其 OAID 能力,把初始化
register()与获取流程Promise 化放到adapters/oaid.uts; - 如果你用的是 gzu 的聚合库(
com.github.gzuliyujiang.oaid),我可按它的 IGetter/Client 回调写成可跑代码;你只需告诉我当前项目里的依赖坐标/使用方式即可(或把现有register/getOAID调用片段贴我)。
- 保留其 OAID 能力,把初始化
zws-uniqueid:- 仅作为 PseudoID 兜底(
adapters/pseudo_id.uts已收编); - 不再把它当“设备唯一标识”,它稳定性不够(但当兜底是有意义的)。
- 仅作为 PseudoID 兜底(
9) 你可能需要的依赖与打包说明(Android)
OAID
- 如果用 MSA 官方 SDK:按其文档接入(添加 AAR、初始化、回调)。
- 如果用 gzu 聚合库:在
module.json的app-android -> utssdk里加配置,或在 HBuilderX 的原生配置里勾选对应依赖;随后把adapters/oaid.uts里按该库接口实现getOAIDRaw()即可。
AAID(Google 广告 ID)
com.google.android.gms:play-services-ads-identifier(需要科学上网/可用镜像);- 通过
AdvertisingIdClient.getAdvertisingIdInfo(context).id获取;若用户限制广告跟踪,返回 limited=true。
我可以根据你环境(国内私服/镜像)给出具体 Gradle 依赖配置方式。
总结
- 这套
uni-id-kit是把你两个 UTS 插件做了统一封装、Promise 化、合规默认与安全加强(哈希/安全随机),并提供清晰可扩展的适配层。 - 你现在就可以把上述文件落地,H5 和 Android 的 AndroidID/GUID/PseudoID 已可用;OAID/AAID 只需在
adapters/*.uts里按你现有库补上调用即可(导出接口已经定好)。 - 一旦接上 OAID,你就能在国内端得到优先使用的稳定设备标识;其余平台自动回退到 AndroidID/IDFV/GUID。
需要的话,你把你现在 OAID 库的 初始化 & 获取代码片段贴我,我直接把 adapters/oaid.uts 写成可用实现版(不再占位)。