ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手机联系人读取不求人:用 TaoToken 统一 Key 打通 id、姓名、手机号提取链路

手机联系人读取不求人:用 TaoToken 统一 Key 打通 id、姓名、手机号提取链路 1. 手机联系人读取为什么总在字段映射上翻车手机联系人读取这件事听起来像是 Android 入门第一课申请权限、拿 ContentResolver 查一把、把 id、姓名、手机号塞进列表就完事。但真到项目里你会发现坑几乎都集中在「字段映射」和「多端一致性」上。Android 这边ContactsContract.CommonDataKinds.Phone一次查询会返回多行——同一个人有两个号码就有两条记录CONTACT_ID相同但_ID不同iOS 那边CNContactStore又是另一套CNContactIdentifierKey、CNPhoneNumbersKey的键值体系。两端字段名对不上导出的 CSV 就会一会儿缺姓名、一会儿手机号串行。更麻烦的是很多团队在写这段逻辑时是「边查边试」先写死列名跑出来发现DISPLAY_NAME为空再换成DISPLAY_NAME_PRIMARY手机号带空格和-又临时加replace。这种逐条手写、逐字段试错的方式在只有一台测试机时还能忍一旦要覆盖多机型、多系统版本维护成本直接爆炸。我这次想做的是把「读取联系人 → 提取 id/姓名/手机号 → 导出 CSV」这条链路做成一次跑通、字段映射固定的骨架。同时用一个统一的 Key 管理方式把 Android、iOS 甚至后续要接的模型辅助清洗环节串起来避免每个端各自维护一套凭证和字段表。这里用到的统一入口是 TaoToken官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 它的 API 地址是 https://taotoken.net/api 后面配置里会具体出现。需要先明确一点联系人数据属于高敏感个人信息本文所有代码只用于本地读取与本地导出不涉及上传、不涉及第三方共享。你在真实项目里也必须先拿到用户明示授权并且只读取业务必需字段。2. TaoToken 前置统一 Key 与联系人链路的定位TaoToken 在这条链路里扮演的不是「读联系人」的角色联系人读取永远由系统 API 完成。它解决的是另一个问题当你需要把读出来的原始字段做规范化、去重、格式校验或者后续要接一个模型来做「姓名与号码的异常检测」时凭证和调用入口是分散的。Android 一个 Key、iOS 一个 Key、脚本一个 Key字段映射表还各写各的很容易乱。TaoToken 提供的是统一的 API Key 与调用入口你可以把它理解成一个「凭证与调用网关」所有需要模型能力的环节都走同一个 base URL 和同一套 Key 体系。这样联系人读取本身仍然是本地系统调用但读取之后的清洗、校验、导出环节可以复用同一套配置字段映射表也能集中维护。具体到操作层面你需要先拿到一个可用的 Key。进入控制台创建 API Key地址是 https://taotoken.net/console/api-keys 创建后复制保存。注意 Key 只在创建时完整显示一次丢了就重新建。如果你只是想先验证模型对话是否通可以用模型对话页面 https://taotoken.net/models 做一次最小请求如果你是要长期跑编码或 Agent 任务建议直接看 Coding Plan https://taotoken.net/coding-plan 额度模型更适合持续调用。这里要强调联系人读取的权限声明、ContentResolver 查询、CNContactStore 请求全部不经过 TaoToken也不应该经过任何外部服务。TaoToken 只在你需要「读取之后的处理」时介入。把边界划清楚后面配置才不会混。3. 可复制配置Android 权限声明与查询骨架先给 Android 端的完整骨架。权限声明放在AndroidManifest.xml里读取联系人需要READ_CONTACTSuses-permission android:nameandroid.permission.READ_CONTACTS /如果是 Android 6.0 以上运行时还要动态申请。下面这段是动态权限请求的最小写法放在 Activity 的onCreate里if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, 1001); }权限拿到后查询骨架如下。注意我用了Phone.CONTACT_ID作为联系人主键而不是Phone._ID因为同一个人多号码时_ID会变CONTACT_ID才稳定public class ContactReader { public static ListContactItem read(Context context) { ListContactItem result new ArrayList(); ContentResolver resolver context.getContentResolver(); String[] projection new String[]{ ContactsContract.CommonDataKinds.Phone.CONTACT_ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_PRIMARY, ContactsContract.CommonDataKinds.Phone.NUMBER }; Cursor cursor resolver.query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, null, null, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_PRIMARY ASC); if (cursor null) return result; try { int idIdx cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID); int nameIdx cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_PRIMARY); int numIdx cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); while (cursor.moveToNext()) { ContactItem item new ContactItem(); item.id cursor.getLong(idIdx); item.name cursor.getString(nameIdx); item.number cursor.getString(numIdx); result.add(item); } } finally { cursor.close(); } return result; } }对应的实体类保持字段名和 CSV 表头一致后面导出就不用再映射public class ContactItem { public long id; public String name; public String number; }这里有个关键点DISPLAY_NAME_PRIMARY比DISPLAY_NAME更稳定系统会优先返回用户设置的主显示名。手机号字段NUMBER可能带空格、括号、-建议在导出前统一清洗但不要在读取时直接改保留原始值便于排查。4. iOS 端字段映射与导出 CSV 的验证动作iOS 端用CNContactStore字段键和 Android 完全不同。请求授权let store CNContactStore() store.requestAccess(for: .contacts) { granted, error in guard granted else { return } let keys: [CNKeyDescriptor] [ CNContactIdentifierKey as CNKeyDescriptor, CNContactGivenNameKey as CNKeyDescriptor, CNContactFamilyNameKey as CNKeyDescriptor, CNContactPhoneNumbersKey as CNKeyDescriptor ] let request CNContactFetchRequest(keysToFetch: keys) try? store.enumerateContacts(with: request) { contact, _ in let id contact.identifier let name \(contact.familyName)\(contact.givenName) for phone in contact.phoneNumbers { let number phone.value.stringValue // 收集 id / name / number } } }字段映射对照表如下方便你两端对齐语义Android 列名iOS 键联系人 idPhone.CONTACT_IDCNContactIdentifierKey姓名Phone.DISPLAY_NAME_PRIMARYGivenNameFamilyName手机号Phone.NUMBERCNPhoneNumbersKey导出 CSV 时表头固定为id,name,number每行做一次转义姓名里如果有逗号或引号用双引号包裹并把内部引号翻倍。下面是一个最小导出函数public static void exportCsv(ListContactItem list, File out) throws IOException { try (BufferedWriter w new BufferedWriter(new FileWriter(out))) { w.write(id,name,number\n); for (ContactItem item : list) { w.write(item.id , escape(item.name) , escape(item.number) \n); } } } private static String escape(String s) { if (s null) return ; if (s.contains(,) || s.contains(\)) { return \ s.replace(\, \\) \; } return s; }验证动作在测试机上跑一次读取导出 CSV用文本编辑器打开确认三列对齐、无串行。如果姓名列出现空值优先检查是不是用了DISPLAY_NAME而不是DISPLAY_NAME_PRIMARY如果手机号列出现多行同 id那是正常的因为一个人多号码导出时按号码展开即可。5. 本篇常见错排查第一个高频错误是权限申请后立刻查询结果 cursor 为空。原因是用户还没点授权或者授权回调还没回来。正确做法是在onRequestPermissionsResult里判断grantCode 1001 grantResults[0] PERMISSION_GRANTED之后再触发查询。第二个错误是列索引取成 -1。getColumnIndex返回 -1 说明 projection 里没有这一列常见于把DISPLAY_NAME_PRIMARY写成了DISPLAY_NAME但系统版本不支持。解决办法是统一用DISPLAY_NAME_PRIMARY并在取索引后加一次判断为 -1 时回退到DISPLAY_NAME。第三个错误是手机号带国家码或格式符导出后无法直接用于短信。建议在导出前做一次规范化但保留原始列例如增加number_raw和number_clean两列避免清洗出错后无法回溯。第四个错误是 iOS 端enumerateContacts在后台线程调用导致 UI 卡顿或数据竞争。联系人枚举是同步阻塞的放到DispatchQueue.global()里执行回到主线程再更新 UI。第五个错误是把联系人数据直接拼进模型请求做「智能清洗」结果把敏感信息发出去了。记住边界清洗逻辑优先本地规则实现确实需要模型辅助时只发送脱敏后的统计特征不发送原始号码和姓名。6. 语义一致 CTA把 Key 和接入文档放在顺手的位置联系人读取本身不需要任何外部 Key但当你把「读取 → 清洗 → 导出」做成可复用链路并且后续要接模型做异常检测或字段补全时统一 Key 的价值就出来了。你可以先在 https://taotoken.net/console/api-keys 创建 Key然后对照接入文档 https://taotoken.net/doc 把 base URL 配成 https://taotoken.net/api 这样 Android、iOS、脚本三端用的是同一套凭证和入口字段映射表也只需要维护一份。如果你只是想先验证模型对话是否可用用 https://taotoken.net/models 做一次最小请求即可如果你是要长期跑编码或 Agent 任务直接看 https://taotoken.net/coding-plan 。Claude Code 相关接入参考 https://taotoken.net/claudecode 。把 Key 管理和联系人读取的边界守住这条链路就能一次跑通、长期可维护。
返回列表