Files
youle_app_ios_v2/docs/SDK-Integration-Guide.md
T
joywayerandClaude Opus 4.7 a2baf742b6 docs:新增 SDK-Integration-Guide.md(微信 / 高德定位 / opencore-amr / 七牛)
按 CLAUDE.md ADR-006「禁 CocoaPods,SPM 优先 + Vendor xcframework」组织,
每个 SDK 章节给出:凭证清单 / 工程接入 / 代码触点 / BackGameData 清理钩子 / 验收 /
风险,便于项目方发凭证后开发者按表落地。

关键约束(写入指南):
- 微信:不复用 msext AppID;AppSecret 客户端泄露风险,fallback 路径上线前必须切后台
- 高德:APIKey 与 Bundle ID 绑死,必须重新申请(msext 的 key 不可用);
  iOS 14+ 隐私合规三步(updatePrivacyShow → updatePrivacyAgree → apiKey)
- opencore-amr:复用 msext 已 segalign 8 修复版 .a,免重编源码;MRC wrapper 加
  -fno-objc-arc 文件级标志
- 七牛:SPM 拉官方 objc-sdk v8.9.x;token 缓存 + 失效前 30s 预刷新
- 每个 SDK 都标明在 BackGameDataHandler 必须加的清理钩子(WXApi.delegate=nil /
  LocationService.stop / AudioRecorder.cancel / QiniuUploader.cancelInFlight)

CLAUDE.md 文档索引追加一行指向新文件。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 21:36:51 +08:00

18 KiB
Raw Blame History

SDK 接入指南

本文档面向项目方 + 开发者,描述 Phase 4.E / Phase 5 / Phase 3.B-D 三组 SDK 的真实接入步骤。

使用方式:每接入一个 SDK 走一遍对应章节的"凭证清单 / 工程接入 / 代码触点 / 清理钩子 / 验收"五步,全程对照 CLAUDE.md「原则 A — H5 端零修改」与 ADR-006「禁止 CocoaPods」。


0. 总览

SDK 触发的契约项 选型 阻塞
微信 OpenSDK accreditlogin / friendsSharetypeUrlToptitleDescript(微信路径)/ sharelogin 反向 callback / sharesuccess Vendor .xcframework 手动 AppID + Universal Links + 后台 /wechat/login(或客户端 fallback
高德定位(AMap startlocation / getlocationinfo 反向 callback9 字段) Vendor .xcframework 手动 新 APIKey(绑死 Bundle ID,不可复用 msext + 隐私合规调用
opencore-amr prepareaudio(录音上传)/ mediaTypeAudio(远端语音回放)AMR ↔ WAV 转码 从 msext 拷贝已修复版 .afat header segalign 8 无外部凭证;二进制已在 msext 仓库内
七牛云上传 prepareaudiogetaudiourl({audiourl, time}) / recordSuccess 反向 callback SPM 官方 https://github.com/qiniu/objc-sdk v8.9.x 后台 token 颁发接口 / 长 token 缓存策略

两条铁律(CLAUDE.md

  1. 任何接入步骤都不允许触碰 H5 端任何文件(包括 app_*.js
  2. 不引入 CocoaPodsSPM 优先,闭源走 Vendor .xcframework

1. 微信 OpenSDKPhase 4.E

1.1 凭证清单(项目方提供)

  • AppID:微信开放平台「移动应用」面板新建一个应用(新 Bundle ID 不能复用 msext 的 wx586a9b321e56efb7,必须为本项目重新申请)
  • Universal Links:项目方拥有的域名 + apple-app-site-association 文件(部署到 https://your-domain.com/.well-known/apple-app-site-associationHTTPS 必须)
  • 后台 /wechat/login 中转接口(强烈推荐):客户端拿 code 调后台,后台用 AppSecretaccess_token + unionid + userinfo,回 7 字段给客户端
    • 若后台未就绪,客户端走 fallback:api.weixin.qq.com/sns/oauth2/access_token + sns/userinfoAppSecret 客户端硬编码 = 安全风险,CLAUDE.md 父项目已明确警告。新项目尽量直接走后台中转
  • 微信 OpenSDK XCFramework:从 微信开放平台官网下载,使用 1.9.x 或最新(避免 1.8.x 的 ITMS-90809 历史包袱)

1.2 工程接入

  1. 放置 SDK

    Vendor/WechatSDK/
      ├── WechatOpenSDK.xcframework
      └── README.md       # 记录版本号 + 下载日期 + 来源
    

    按 Design §14.1 Vendor 接入标准流程:Target → General → Frameworks → Embed & Sign

  2. Info.plist 改动(仅原生侧,不动 H5

    <!-- 1. URL Scheme:让微信 App 回调能打开本 App -->
    <key>CFBundleURLTypes</key>
    <array>
      <dict>
        <key>CFBundleURLName</key>
        <string>weixin</string>
        <key>CFBundleURLSchemes</key>
        <array><string>YOUR_WX_APPID</string></array>
      </dict>
    </array>
    
    <!-- 2. 查询微信是否安装 -->
    <key>LSApplicationQueriesSchemes</key>
    <array>
      <string>weixin</string>
      <string>weixinULAPI</string>
      <string>weixinURLParamsAPI</string>
      <!-- 已有的 mqq* / snssdk1128 别覆盖 -->
    </array>
    
  3. Entitlement 改动Signing & Capabilities

    • Associated Domains 添加 applinks:your-domain.com(与 apple-app-site-association 的域名严格一致)

1.3 代码触点(接入此 SDK 必须改的文件)

文件 改动
Source/SDK/WeChat/WeChatSDK.swift(新建) WXApi.registerApp(_:universalLink:) 启动注册;包含全部 12 个 MMAPP_SUPPORT_* flagDesign §4.2
AppDelegate.swift application(_:didFinishLaunchingWithOptions:) 内调 WeChatSDK.register()
SceneDelegate.swift openURLContexts 内按 QQ → WXApi 顺序判(msext AppDelegate.m 顺序硬约束,QQ 必须先判)
Source/SDK/WeChat/WeChatManager.swift(新建) 持久 delegateauthorize() async throws -> WXAuthCodestate UUID 配对);share(_:scene:) async throwsFIFO 串行)。Design §8.5
Source/Login/WeChatAuth.swift(新建) authorize() 拿 code → 转后台 /wechat/login(或 fallback 拼 sns/oauth2/access_token + sns/userinfo)→ 拿到 7 字段 user。Fallback 文件头标 // FIXME: 待后台 /wechat/login 就绪后切换为后台中转
Source/Bridge/Handlers/AccreditLoginHandler.swift 移除 stub 返回,调 WeChatAuth.authorize() → 反向 bridge.call("sharelogin", 7 字段)
字段名严格 1:1(含历史包袱)openid / headimgurl / nickname / sex / city / Province大写 P!)/ unionid
Source/Share/WechatShare.swift 移除 stub return .success,调 WeChatManager.shareisInstalled 改为 WXApi.isWXAppInstalled()
Source/Bridge/Handlers/BackGameDataHandler.swift 必须加 WXApi.delegate = nil(清理子游戏 push 期间的 delegate 指针)

1.4 清理钩子(CLAUDE.md 第 6.0 节回响)

BackGameDataHandler.swiftMainActor.run 块加一行:

WXApi.delegate = nil    // ← Phase 4.E 接入时取消注释

理由:msext gameController.m:699 在 backgameData 里清 delegate,防止 pop 后微信回调指向已销毁的 SubGameVC。

1.5 验收(H5 端零修改前提下跑)

  • H5 调 bridge.callHandler('accreditlogin') → 拉起微信 → 同意授权 → 大厅收 sharelogin({openid,...,Province})Province 是大写 P
  • H5 调 bridge.callHandler('friendsSharetypeUrlToptitleDescript', {sharefriend:"2", ...}) → 直接调起微信朋友圈
  • H5 调 sharefriend:"1" → SharePanel 三按钮,点微信 → 拉起微信好友选择 → 分享成功后大厅收 sharesuccess({success:"2",type:"1"})
  • 关键回归:截图分享 / QQ 分享 / 抖音分享路径不受影响

1.6 风险

  • AppSecret 客户端泄露:fallback 路径上线前必须切到后台中转,否则 secret 一旦泄露不可重置(CLAUDE.md 父项目「已识别为安全风险,明确接受不修」的同款问题,新项目从一开始避免)
  • Universal Links 配置错误:授权回不来。验证方式:xcrun simctl openurl booted "https://your-domain.com/test" 看是否拉起本 App
  • 微信审核:审核期间 OpenSDK 行为差异(部分 API 限频),上线初期保持后台监控

2. 高德定位 SDKPhase 5

2.1 凭证清单

  • 新 APIKey高德控制台 用项目方账号新建应用,绑定本项目的 Bundle ID(msext 的 b0d4a8e3fcbbcc0dd96283b7df6a4494 不能复用APIKey 与 Bundle ID 一一绑死)
  • AMap SDK XCFramework官方下载页拉取最新(截至 2026-06 推荐 v2.10.x+
    • AMapLocationKit.xcframework — 定位主库
    • AMapFoundationKit.xcframework — 基础库(必须配合 LocationKit

2.2 工程接入

  1. 放置 SDK

    Vendor/AMap/
      ├── AMapLocationKit.xcframework
      ├── AMapFoundationKit.xcframework
      └── README.md     # 记录 SDK 版本 + APIKey 创建日期 + 绑定 Bundle ID
    

    Target → General → Frameworks → Embed & Sign(两个都要)

  2. Info.plist(仅原生侧)

    <key>NSLocationWhenInUseUsageDescription</key>
    <string>{gamehallname}需要访问您的位置以提供本地化服务</string>
    

    {gamehallname} 占位由 ChannelConfig.plist 注入或硬编为渠道名。

2.3 代码触点

文件 改动
Source/SDK/AMap/AMapWrapper.swift(新建) 启动期调用顺序:updatePrivacyShow(.didShow, privacyInfo: .didContain)updatePrivacyAgree(.didAgree)AMapServices.shared().apiKey = "..."iOS 14+ 隐私合规必走的 3 步,少一步定位拿不到结果)
AppDelegate.swift didFinishLaunchingWithOptions 内调 AMapWrapper.bootstrap()
Source/Location/LocationService.swift(新建,actor requestOnce() async throws -> LocationPayload(一次定位)
startContinuous(onUpdate:) / stop()(持续定位)
AMapLocationManager,回调转 async
Source/Bridge/Handlers/StartLocationHandler.swift 移除 stub,按 data == 1 持续 / 其它一次性
反向 callback bridge.call("getlocationinfo", 9 字段)
字段名严格 1:1address / city / cityCode / country / district / latitudestringString(format: "%f", lat)/ longitudestring/ province小写 p!与 sharelogin.Province 不同)/ street
失败:getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})
Source/Bridge/Handlers/BackGameDataHandler.swift 必须加 LocationService.shared.stop()

2.4 清理钩子

BackGameDataHandler.swiftMainActor.run 块加:

LocationService.shared.stop()    // ← Phase 5 接入时取消注释

理由:msext gameController.m:2473-2478 cleanUpAction 在 backgameData 里调 [locationManager stopUpdatingLocation] + delegate nil;防止 pop 后后台定位继续耗电 + 回调悬空。

2.5 验收

  • 首次启动 → 系统权限弹框(文案为 Info.plist 设置的中文)
  • H5 调 bridge.callHandler('startlocation', "1") → 收到 getlocationinfo({...9 字段...})latitude / longitude 是 string 类型,province 是小写 p
  • 拒绝权限 → H5 收 getlocationinfo({errorCode:12,errorMsg:"缺少定位权限"})
  • 子游戏定位中 → 调 backgameData 退出 → 大厅不再收到悬挂定位回调(验证 cleanUp 钩子)

2.6 风险

  • APIKey 绑定Bundle ID 改了 / 多 Target 用不同 ID 都要重新申请 key,否则定位失败但无明显报错(高德回 errorCode 7「KEY 校验失败」)
  • 隐私合规调用顺序iOS 14+ 必须先 updatePrivacyShowupdatePrivacyAgreeapiKey,否则定位接口直接拒绝
  • 模拟器:高德定位在模拟器走 Apple 的 CoreLocation 模拟坐标,定位精度低,必须真机测

3. opencore-amr 转码库(Phase 3.B

3.1 凭证清单

  • 无凭证。二进制已在 msext 仓库内,且已经过 fat header segalign 8 修复(参 CLAUDE.md 父项目「已处理的定时炸弹」一节,2026-06-20 摘掉 -ld_classic 的同一组 .a

3.2 工程接入

  1. 拷贝二进制(已修复版)

    mkdir -p ylgamehall/Vendor/opencore-amr/
    cp ../daoqi/msext/msext/Class/Common/VoiceConvert/lib/libopencore-amrnb.a ylgamehall/Vendor/opencore-amr/
    cp ../daoqi/msext/msext/Class/Common/VoiceConvert/lib/libopencore-amrwb.a ylgamehall/Vendor/opencore-amr/
    # 拷头文件
    cp -r ../daoqi/msext/msext/Class/Common/VoiceConvert/include ylgamehall/Vendor/opencore-amr/
    
  2. 拷 VoiceConverterObjC wrapper

    ylgamehall/Vendor/opencore-amr/VoiceConverter/
      ├── VoiceConverter.h    # ObjC interface(保留 ObjCwrapper 简单不重写 Swift
      ├── VoiceConverter.m    # MRC 代码可保留,加 -fno-objc-arc 编译标志或转 ARC
      ├── amrFileCodec.h/m
      └── interf_*.h/c
    
  3. Xcode 工程配置

    • Target → Build Phases → Link Binary With Libraries 加入两个 .a
    • Target → Build Settings → Library Search Paths 加 $(PROJECT_DIR)/ylgamehall/Vendor/opencore-amr
    • Target → Build Settings → Header Search Paths 加 $(PROJECT_DIR)/ylgamehall/Vendor/opencore-amr/include
    • 不需要 -ld_classic(已用 lipo -segalign 8 修复 fat headerCLAUDE.md 父项目记录的修复同样适用)
    • 若复用 msext MRC 的 VoiceConverter.m,加文件级 -fno-objc-arc 编译标志
  4. Bridging Header(已有 ylgamehall-Bridging-Header.h 则追加;无则新建)

    #import "VoiceConverter.h"
    

3.3 代码触点

文件 改动
Source/Audio/VoiceCoder.swift(新建) Swift wrapperstatic func amrToWav(_ amrPath: URL) throws -> URL / static func wavToAmr(_ wavPath: URL) throws -> URL
内部调 ObjC VoiceConverter
Source/Audio/AudioRecorder.swift(新建,actor record() async throws -> AudioFileAVAudioRecorder 录 WAV + 麦克风权限
录音参数与 msext ChatVoiceRecorderVC 等价:16kHz / 单声道 / 16-bit
Source/Bridge/Handlers/RemoteAudioHandler.swift 移除 stub,分别接入:
prepareaudio(录音上传)→ AudioRecorder.record() → WAV→AMR VoiceCoder.wavToAmrQiniuUploader.upload(见 §4bridge.call("getaudiourl", {audiourl, time})子游戏页额外触发 bridge.call("recordSuccess", {fileUrl, fileName, fileKey})
mediaTypeAudio(远端回放)→ URLSession 下载 AMR → AMR→WAV VoiceCoder.amrToWavAVAudioPlayer 播放
开始播放 bridge.call("gameui_play_voice", user) / 播放结束 bridge.call("gameui_stop_voice", user)
VoicePlayingHandler 的开关短路(=1 才播)
Info.plist NSMicrophoneUsageDescription = "{gamehallname}需要访问您的麦克风录制语音消息"
Source/Bridge/Handlers/BackGameDataHandler.swift 必须加 AudioRecorder.shared.cancel()

3.4 清理钩子

AudioRecorder.shared.cancel()        // ← Phase 3.C 接入时取消注释
QiniuUploader.shared.cancelInFlight() // ← Phase 3.C 接入时取消注释

理由:录音 / 上传任务跨 VC 生命周期容易遗留;msext 通过 ChatVoiceRecorderVC 生命周期间接清理,我们的 actor 实现不绑 VC 必须显式 cancel。

3.5 验收

  • H5 调 prepareaudio → 弹麦克风权限 → 同意后开始录音 → 录完上传七牛 → 大厅收 getaudiourl({audiourl:"https://...", time:"3"})
  • 子游戏同样路径,额外收到 recordSuccess({fileUrl, fileName, fileKey})
  • H5 调 mediaTypeAudio({audiourl, user}) → 听到对方语音 + 收到 gameui_play_voice / 结束收 gameui_stop_voice
  • voicePlaying 总开关设 0 → 调 mediaTypeAudio 不播放

4. 七牛云上传(Phase 3.C / 3.D

4.1 凭证清单

  • 后台 token 颁发接口:每次录音前向后台 GET /api/qiniu/upload-token?scene=voice 拿到上传凭证 + 域名
  • 七牛 Bucket 名 / CDN 域名:项目方提供(可硬编到 BundleConfig,或随 ChannelConfig 注入)

4.2 工程接入(SPM,优于二进制)

  1. Xcode → File → Add Package Dependencies
  2. URLhttps://github.com/qiniu/objc-sdk
  3. 版本:8.9.x 最新(avoid 7.x 历史 API
  4. 勾选 target → Add Package

注:七牛官方 SPM 包是 ObjC,但 SPM 直接支持 ObjC + Swift 混合,不需要 bridging headerSPM 自动处理)。

4.3 代码触点

文件 改动
Source/Network/QiniuTokenService.swift(新建,actor fetchUploadToken() async throws -> QiniuToken(含 token / 上传域名 / Key 模板 / 过期时间)
缓存策略:token 失效前 30 秒强制刷新;并发请求合并
Source/Network/QiniuUploader.swift(新建,actor upload(_ file: URL, token: QiniuToken) async throws -> UploadedFile
内部用 QNUploadManager 包成 asyncUploadedFile 含 fileUrl / fileName / fileKey
取消语义:cancelInFlight() 取消所有进行中任务(接清理钩子用)
Source/Bridge/Handlers/RemoteAudioHandler.swift prepareaudio 完整链路第 3 步:QiniuUploader.upload(amrFile, token: await QiniuTokenService.shared.fetchUploadToken())

4.4 清理钩子

见 §3.4(与 opencore-amr 同一处加 QiniuUploader.shared.cancelInFlight())。

4.5 验收

  • H5 调 prepareaudio → 录音 3 秒 → 上传 → 大厅收 getaudiourl({audiourl:"https://your-cdn.com/voice/xxx.amr", time:"3"})
  • 录音中 / 上传中 → 退出子游戏 → 任务被 cancel,不再有悬空网络写
  • token 过期场景:模拟后台返回过期 token → uploader 自动刷新重试一次

4.6 风险

  • token 时效:通常 1 小时;高频录音场景需要 caching + 失效前预刷新
  • 上传失败重试:默认 3 次,超时建议设 30s(语音文件通常 < 100KB
  • CDN 域名 HTTPSH5 业务侧拿到 audiourl 后会请求播放,必须保证域名 HTTPSNSAppTransportSecurity 我们没收紧 ATS,但七牛默认走 HTTPS)

5. 接入完毕的清单检查

每接入完一个 SDK,跑一遍:

  • BuildProject 成功
  • Vendor/<SDK>/README.md 已记录版本号 / 来源 / 凭证创建日期
  • Info.plist 改动只在原生侧,未触 H5 任何文件
  • BackGameDataHandler.swift 对应清理钩子已加(每个 SDK 都要回到此处补 1 行)
  • docs/Development-Plan.md §5 / §8 进度勾选已同步(提交时附 commit 备注「Plan 进度已勾选」)
  • docs/Verification-Checklist.md 对应 Phase 验证项已补充
  • 真机跑过该 SDK 的验收项(模拟器对 SDK 行为不可靠:高德、微信、AVAudioRecorder 都受影响)

6. 与原项目 daoqi 的关系

接入过程中若行为与文档描述不一致,优先参考原 daoqi 工程实现(参 CLAUDE.md「原项目 daoqi:遇到问题时的参考来源」一节):

  • 微信回调字段:daoqi/msext/Class/RootVC/NewRootVC.m:2428sharelogin 7 字段,含 Province 大写)
  • 高德 9 字段:daoqi/msext/Class/RootVC/gameController.m:2528 / 2557getlocationinfo 字段顺序与类型)
  • AMR 转码:daoqi/msext/Class/Common/VoiceConvert/VoiceConverter.h/mamrFileCodec.h/m
  • 七牛上传:daoqi/msext/Class/Utils/QiniuManager.m(参数命名 + 回调结构)

遇到契约边界 / 历史包袱时:照搬 msext 字面 + 字段名 + 顺序,不要凭语义猜测。原项目稳定运行多年的写法是真理,文档 / 设计稿可能有传抄错误(已在 CLAUDE.md「典型案例」积累若干)。