前阵子接手一个活儿要把已经跑了三年的内部办公 App 接进浙政钉实现扫码登录、免登跳转和消息推送这三件事。听上去就是装个 SDK的活儿真上手才发现光安装这一环就够折腾渠道包不通用、签名校验卡死、混淆之后回调莫名其妙失灵、多进程重复初始化……前后踩了差不多两周。这篇就把浙政钉 SDK 安装这条链路从头到尾捋一遍包括环境准备、依赖引入、初始化和鉴权时序以及我实际遇到的十来个报错和对应解法。如果你手上正好有个 App 要往这个平台接或者团队里没人干过这事被你赶鸭子上架那这篇东西应该能帮你省掉大半的排查时间。1. 先拆清楚浙政钉 SDK 到底装的是什么1.1 SDK 的能力边界与交付形态很多人第一反应是去官网下个安装包双击装一下这个思路从根上就偏了。浙政钉 SDK 不是一个独立可运行的程序而是一组按能力切分的客户端组件集合外加一套服务端 HTTP 接口规范。它要装进的是你自己的 App 工程而不是某个系统目录。这一点想不明白后面所有的配置都会找不到方向。客户端这边的交付形态通常有三种Android 侧以 aar 包为主偶尔配一个单独的 so 库包iOS 侧是 framework 或 xcframework通过 CocoaPods 或者手动拖入工程H5 微应用侧则是一份 JS 文件由容器在页面加载时自动注入你只需要调用dd.xxx这类挂载在全局对象上的方法。服务端侧没有包的概念你能拿到的只有接口文档和一对 appKey/appSecret 凭证。能力上大致可以分成五类。第一类是身份类免登授权和扫码登录都在这里核心是拿免登码去服务端换用户身份。第二类是消息类工作通知推送、会话消息发送。第三类是原生能力类扫一扫、拍照、定位、通讯录选人这些本质是让宿主 App 的容器能力暴露给你。第四类是事件回调类组织架构变更、应用可见范围调整这类通知。第五类是数据类通过服务端接口读组织和人员信息。这里有个新手最容易误解的点客户端 SDK 基本只负责唤起和回调真正拿到用户是谁、属于哪个部门是靠服务端拿免登码去换的。所以安装阶段就要把客户端和服务端当成一个整体来规划只做客户端或者只做服务端最后一定跑不通。1.2 三种接入形态的取舍选哪种接入形态决定了你后面安装步骤的复杂度和长期维护成本。这张表是我自己项目里总结的对比供参考。接入形态适用场景开发成本可用原生能力发版节奏调试难度客户端原生集成已有独立 App要打通身份和推送高全部跟随 App 发版高需真机签名H5 微应用内部轻量工具、表单审批类低仅 JSAPI 暴露的随时更新低浏览器容器纯服务端对接后台数据同步、组织架构同步中无独立部署中日志即可为什么这么分因为浙政钉的客户端本身就是一个超级容器它把系统能力做了封装再对外开放。你走原生集成相当于把自己塞进这个容器里能力和宿主一样全但代价是必须跟着它的版本走它升级大版本你就得回归测试。你走 H5相当于租了容器的几个窗口开发快、改起来也快但一旦需要蓝牙、后台定位这种深度能力就没辙。我的建议是核心业务走原生运营活动和临时工具走 H5两条腿并行。千万别为了省事把所有东西都塞 H5等业务方提一个要读本地相册原图的需求时你会很尴尬。反过来也别什么都原生一个活动页要发版审核两周业务方会疯。2. 安装前的环境准备与依赖梳理2.1 版本矩阵必须一次性对齐SDK 安装失败十次有七次栽在版本不对齐上。Android 侧至少要确认这几个JDK 版本、Gradle 版本、Android Gradle Plugin 版本、compileSdk、minSdk、targetSdk如果 SDK 里带 so 库还要确认 ABI 过滤配置。iOS 侧则是 Xcode 版本、CocoaPods 版本、最低支持的 iOS 版本、以及是否需要开启 Bitcode。为什么这么强调对齐因为 aar 包里通常带着它自己编译时的依赖声明。如果你的工程还在用老的支持库而 SDK 依赖的是 AndroidX构建时资源合并会直接报Duplicate class或者Program type already present。我遇到过一次最典型的工程里android.useAndroidXfalse结果引入 SDK 之后 build 直接崩在 mergeDebugResources 阶段日志刷了三千行。判断方法很简单打开 aar 里的AndroidManifest.xml和classes.jar看一眼它引了什么。命令行一条就够unzip -o your-sdk.aar -d sdk_extract cat sdk_extract/AndroidManifest.xml unzip -l sdk_extract/classes.jar | head -50iOS 那边同理otool -L YourSDK.framework/YourSDK看一下它链接了哪些系统库和第三方库避免和工程里已有的库版本打架。提示版本对齐这件事不要靠猜。把 SDK 文档里给的版本矩阵抄进项目的 README每次升级 SDK 都对照一遍比事后查崩因快十倍。2.2 凭证申请与签名指纹这是很多人会忽略的一步SDK 在初始化阶段就会做调用方校验校验依据就是包名加签名指纹。所以你在写第一行集成代码之前就得把这两样东西准备好否则初始化会静默失败日志里只有一行含糊的参数错误。Android 侧需要的是应用包名和签名证书的 MD5 指纹。取指纹的命令keytool -list -v -keystore your_release.jks -alias your_alias -storepass 你的密码输出里找 MD5 那一行去掉冒号转成小写就是平台要的格式。这里有个坑debug 包和 release 包的签名指纹不一样测试环境和生产环境要分别申请或者干脆用同一套配置。我见过团队只申请了 release 的结果开发同学本地一跑就报授权失败查了半天。iOS 侧要的是 Bundle ID以及如果用到跳转回 App 的能力还要配 Universal Links 的域名和 apple-app-site-association 文件。这个文件必须放在域名的根路径Content-Type是application/json且不能有任何重定向否则 iOS 直接不认。服务端凭证是 appKey 和 appSecret这个只在服务端保存绝对不要塞进客户端代码里。我之前 review 过一个项目appSecret 硬编码在 Android 的 BuildConfig 里反编译一下就能看到等于把后台接口的钥匙挂在门口。2.3 依赖仓库与构建环境如果 SDK 只给了本地 aar那你需要决定是自己搭个私服还是直接放 libs 目录。放 libs 目录最快但传递依赖会丢后面要手动补一堆implementation。用私服的话把 aar 用maven-publish推上去依赖关系就能自动解析。内网环境还要注意仓库地址和代理配置。Gradle 的repositories里如果只写了公司私服而 SDK 依赖的某个开源库不在私服里构建会直接超时。稳妥做法是私服放前面后面兜底加公共仓库。repositories { maven { url https://your-nexus/repository/maven-releases/ } mavenCentral() google() }3. Android 端安装与集成实操3.1 依赖引入的两种方式及其代价本地 aar 引入的写法android { repositories { flatDir { dirs libs } } } dependencies { implementation(name: zjz-sdk-1.0.0, ext: aar) // flatDir 不解析 pom传递依赖需要手动补 implementation com.squareup.okhttp3:okhttp:4.9.3 implementation com.google.code.gson:gson:2.8.9 }flatDir最大的问题是它不读 pom 文件也就是说 SDK 里声明依赖的第三方库全部不会自动拉下来你得自己一个个补。漏一个的后果通常是运行时NoClassDefFoundError编译期完全看不出问题。所以我更推荐第二条路把 aar 转成 maven 坐标推到私服。# 用 maven-publish 插件发布本地 aar 到私服 ./gradlew publishReleasePublicationToMavenRepository发布之后依赖就变成一行干净的坐标传递依赖自动解析版本冲突也能用./gradlew app:dependencies查出来。3.2 ABI 过滤、权限与混淆规则如果 SDK 带 so 库先看一下它提供了哪些架构。启动 x86 模拟器调试时如果 SDK 没有 x86 版本会在System.loadLibrary那里崩掉。解决办法是给模拟器装 ARM 翻译或者直接用真机。android { defaultConfig { ndk { abiFilters armeabi-v7a, arm64-v8a } } }权限方面SDK 的 Manifest 会自动合并进来一部分但相机、存储、定位这类危险权限通常要你在业务侧自己申请。合并之后建议打开app/build/intermediates/merged_manifests/看一眼最终结果确认没有意外的权限被引入。上线前如果被审核问到为什么申请通讯录权限你得答得上来。混淆规则是另一个重灾区。SDK 内部大量使用反射和 JS bridge类名被裁掉之后回调直接失灵而且不报错就是没反应。所以必须保留-keep class com.zjz.** { *; } -keepclassmembers class com.zjz.** { *; } -dontwarn com.zjz.** -keep class * extends com.zjz.callback.BaseCallback { *; }注意不要图省事写-keep class com.** { *; }那等于没混淆包体积和安全性都受影响。要精确到 SDK 的包名前缀。3.3 初始化时序与多进程陷阱初始化必须放在Application.onCreate里而且要早于任何一次 SDK 调用。但这里有个隐蔽的坑如果你的 App 有推送进程、常驻进程Application.onCreate会被调用多次SDK 就被重复初始化。有些 SDK 会抛异常有些会静默覆盖状态导致主进程的回调收不到消息。标准做法是先判断进程名public class MyApp extends Application { Override public void onCreate() { super.onCreate(); if (isMainProcess()) { ZjzSdk.init(this, appKey, new InitCallback() { Override public void onSuccess() { // 初始化完成后才能调免登 } Override public void onFailure(int code, String msg) { Log.e(ZjzSdk, init failed: code msg); } }); } } private boolean isMainProcess() { String processName getProcessName(); return processName ! null processName.equals(getPackageName()); } }getProcessName在不同 Android 版本上实现不一样Android 9 以上可以用Application.getProcessName()低版本要读/proc/self/cmdline。这段代码几乎每个项目都要写一遍建议直接抽成工具类。初始化回调里有个容易忽略的点onSuccess之后才能调免登和扫码。如果你在onCreate里初始化完就立刻调免登大概率拿到的是SDK 未就绪。稳妥做法是把初始化完成的信号用一个标志位或者CountDownLatch存起来业务侧调用前先等这个信号。4. iOS 端安装与配置实操4.1 CocoaPods 集成与静态库选择Podfile 里引入 SDK 通常有两种形式一种是官方源一种是本地 podspec 或直接拖 framework。用 Pod 的好处是版本管理和依赖解析都省心。platform :ios, 12.0 use_frameworks! :linkage :static target YourApp do pod ZjzSDK, ~ 1.0.0 enduse_frameworks!后面的:linkage参数很关键。如果 SDK 是静态库而你用动态链接会出现符号重复反过来如果 SDK 内部依赖了动态库而你强制静态链接会报错。判断方法还是那句otool -L或者直接问 SDK 提供方要一份标准 Podfile 示例。国内网络环境下pod install卡在CDN: trunk Repo update是常态可以临时改成用国内镜像源或者--verbose看卡在哪一步。这一步没有技术含量但很耗时间建议第一次装的时候就配好。4.2 白名单、URL Scheme 与关联域名iOS 的沙箱机制决定了跳出去再跳回来这件事必须提前声明。如果要唤起宿主 App 的扫码页需要在Info.plist里配置LSApplicationQueriesSchemes把要查询的 scheme 加进去否则canOpenURL永远返回 false。keyLSApplicationQueriesSchemes/key array stringzjz/string stringdingtalk/string /array如果要让宿主 App 跳回你的 App需要在CFBundleURLTypes里注册自己的 scheme同时如果要走 Universal Links还得配com.apple.developer.associated-domains权限并在服务端放好关联文件。这两套机制建议都配上scheme 作为兜底Universal Links 作为主路径。回跳的接收在AppDelegate里处理func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] [:]) - Bool { return ZjzSDK.handleOpenURL(url) }如果是 SceneDelegate 架构对应的方法要挪到scene(_:openURLContexts:)里。这个迁移坑了很多从老项目升级上来的团队表现就是回跳没反应日志也没有。4.3 初始化与鉴权调用时序iOS 的初始化和 Android 类似放在didFinishLaunchingWithOptions里拿 launchOptions 里的信息做前置处理。时序上的严格程度甚至更高因为 iOS 的 UIApplication 生命周期更强调顺序。一个常见问题是初始化返回成功但免登拿不到码返回值。八成是因为你调用的 URL 没有在关联域名里或者associated-domains权限没开。检查顺序建议是先看 Xcode 里的 capability 有没有勾上再看服务端的关联文件能不能直接 curl 到最后再看签名证书和 Bundle ID 是否和申请时一致。5. H5 微应用与服务端配套要点5.1 JSAPI 注入与鉴权签名H5 微应用不需要安装包容器会在页面加载时注入 JS 桥。你只需要在合适时机调用但调用前必须先做鉴权签名这一步是服务端完成的。服务端的流程是先用 appKey 和 appSecret 换 access_token再用 access_token 换 jsapi_ticket然后把jsapi_ticket、nonceStr、timestamp、当前页面 URL去掉 # 后面的部分按字段名排序拼接成字符串做 SHA1 得到签名。前端拿到签名后调用配置接口之后才能用其他 JSAPI。// 前端拿到服务端返回的签名配置后 dd.config({ agentId: xxx, corpId: xxx, timeStamp: xxx, nonceStr: xxx, signature: xxx, jsApiList: [runtime.permission.requestAuthCode, biz.util.scan] }); dd.ready(function () { dd.runtime.permission.requestAuthCode({ corpId: xxx, onSuccess: function (res) { // 把 res.code 传给自己的服务端换用户身份 } }); });签名失败最常见的原因是 URL 拼接不对。浏览器地址栏里的 URL 和容器实际传给 SDK 的 URL 经常不一致尤其是带了路由参数或者被 Nginx 重写过的情况。排查方法是在签名接口里把参与签名的原始 URL 打日志和dd.error回调里返回的 URL 对比一个字符一个字符地对。5.2 服务端 token 缓存策略access_token 有有效期一般是两小时且同一个应用多次获取会有频率限制。所以必须缓存不能每次请求都去换。缓存方案很简单Redis 存一个 key值就是 token过期时间设成比实际有效期少五分钟避免临界点失效。import time import redis r redis.Redis() def get_access_token(app_key, app_secret): cached r.get(fzjz:token:{app_key}) if cached: return cached.decode() resp requests.get(TOKEN_URL, params{ appkey: app_key, appsecret: app_secret }).json() token resp[access_token] expires resp.get(expires_in, 7200) r.setex(fzjz:token:{app_key}, expires - 300, token) return token def get_jsapi_ticket(token): cached r.get(fzjz:ticket:{token[:8]}) if cached: return cached.decode() resp requests.get(TICKET_URL, params{access_token: token}).json() ticket resp[ticket] expires resp.get(expires_in, 7200) r.setex(fzjz:ticket:{token[:8]}, expires - 300, ticket) return ticket这里有个细节换 token 和换 ticket 这两个接口如果并发调用会互相顶掉对方的旧值导致后拿到的那个失效。稳妥做法是加一把分布式锁或者干脆用一个定时任务每 90 分钟刷一次业务侧只读缓存。6. 踩过的坑与排查速查表6.1 构建期常见报错这张表是我自己记录过的基本覆盖了构建阶段 90% 的问题。报错信息关键词根因处理方式Duplicate class / Program type already present支持库与 AndroidX 混用打开android.useAndroidXtrue全量迁移Could not find :zjz-sdk:仓库地址没配或 aar 没上传检查repositories顺序确认私服里有这个坐标No such property for ABIso 库架构不匹配用abiFilters过滤或换真机调试Undefined symbols for architecture arm64iOS 库架构缺失确认 xcframework 包含 arm64 切片Linker command failed with exit code 1静态库与动态库链接方式冲突调整 Podfile 的:linkage参数Module not foundPod 没 install 或 Header Search Path 不对重新pod install检查配置6.2 运行期常见故障运行期的坑更隐蔽因为很多失败是静默的。免登回调不触发、扫码返回空、推送收不到这三类占了我遇到问题的大头。免登回调不触发先查初始化是否真的完成。加日志打一下初始化回调如果压根没回调说明校验就失败了多数是签名指纹不对或者包名不匹配。扫码返回空通常是目标 App 没装或者白名单没配。推送收不到分两层看客户端看是否成功注册了推送通道服务端看推送接口返回码是不是用户不在可见范围内。有个特别坑的现象debug 包一切正常release 包一打就全崩。原因几乎肯定是混淆。花十分钟把-keep规则补全比事后拿着一份线上崩溃日志猜半天强得多。提示建议在 SDK 初始化回调里把返回码和描述完整打到日志里并在测试包里加一个打印 SDK 版本和当前配置的调试入口。真出问题时这两个信息能帮你少走很多弯路。6.3 排查工具与日志位置Android 侧adb logcat过滤 SDK 的 TAG 是最直接的手段。如果 SDK 日志被关掉了可以用adb shell setprop log.tag.ZjzSdk VERBOSE打开。iOS 侧Xcode 的 Console 加上设备日志Xcode 菜单里的 Devices and Simulators配合看一些容器层面的日志只在设备日志里有。网络层建议挂个抓包工具把免登换码、token 换取这几个请求的入参出参都看一眼。很多时候服务端返回的 JSON 里明明有错误码只是前端没打出来白白排查半天。7. 上线前自检与几句实在话上线前我会过一遍这张清单你也可以照着走。包名和签名指纹是否和申请时一致release 和 debug 是否都验证过混淆规则是否覆盖 SDK 全部包名前缀并且测试过 release 包初始化是否只在主进程执行多进程场景是否验证免登、扫码、推送三条主链路是否在真机上完整跑通服务端 token 缓存是否生效有没有做并发保护iOS 的白名单、URL Scheme、关联域名是否三条都配齐。还有一点值得单独说SDK 版本升级不要跟得太紧。新版本刚发布时社区反馈的坑还没出来你贸然升上去踩雷的概率远大于收益。我的习惯是等一个小版本比如从 1.0.0 到 1.0.1 之后再看同时在自己的测试环境把新旧版本的回调行为对比一遍。最后说个我觉得最有价值的经验。这套集成的难点其实不在装而在断。客户端和服务端之间的边界、主进程和子进程之间的边界、宿主和容器之间的边界三条边界只要有一条没理清问题就会以极其反直觉的形式冒出来。我在项目里做的一件事是画了一张时序图贴在工位上把用户点击扫码到服务端拿到用户身份中间经过的每一个环节和每一次跨进程、跨应用跳转都标出来。后来组里新人接手靠着这张图两天就定位了一个困扰我们三天的问题。工具和文档会变这套把链路拆到最细的思路不会变。