Files
youle_app_ios_v2/docs/SDK-Integration-Guide.md
T
joywayerandClaude Opus 4.7 1d7e258130 Phase 4.E + Phase 5 + Phase 3.C 七牛上传:代码侧 SDK 接入(SPM 优先 / Vendor 兜底)
选型(CLAUDE.md ADR-006「SPM 优先 + Vendor xcframework 兜底」):
- 微信 → SPM(Tencent 官方 https://github.com/Tencent/WechatOpenSDK-XCFramework);
  项目方下载的 WechatOpenSDK-NoPay.xcframework 已撤出 Vendor,docs/res 内副本作离线备份
- 高德 → Vendor 手动(官方未提供 SPM;Vendor/AMap/{AMapFoundationKit,AMapLocationKit}.framework
  随仓库分发,fat .framework 同时含 x86_64 + arm64,Do Not Embed)
- 七牛 → SPM(https://github.com/qiniu/objc-sdk v8.9.x)

代码侧(全部 #if canImport 守卫,Xcode 加 SDK 前编译为 no-op):
- Source/SDK/WeChat/WeChatSDK.swift:WXApi.registerApp + handleOpenURL(沿用 msext AppID
  wx586a9b321e56efb7,universalLink 空字符串走非 ULAPI 路径)
- Source/SDK/WeChat/WeChatManager.swift:WXApiDelegate + state UUID 配对的 authorize +
  FIFO 串行 share async/await wrapper
- Source/Login/WeChatAuth.swift:客户端直拼 sns/oauth2/access_token + sns/userinfo →
  7 字段(Province 大写 P / city 经 danbian 去单引号),沿用 msext 同款路径
- Source/SDK/AMap/AMapWrapper.swift:iOS 14+ 隐私合规 3 步 + apiKey 注入
- Source/Location/LocationService.swift:actor + AMapLocationManager 异步包装 + 9 字段
- Source/Network/QiniuConfig.swift:4 项常量沿用 msext(AccessKey/SecretKey/Bucket/Domain)
- Source/Network/QiniuTokenSigner.swift:纯 Swift CryptoKit HMAC-SHA1 + Base64URL 自签
  token(与 msext QiniuManager.m:200-230 等价,不依赖 Qiniu SDK 工作)
- Source/Network/QiniuUploader.swift:actor 包 QNUploadManager async/await

Handler 升级:
- AccreditLoginHandler:拉起授权 → 反向 callback sharelogin 7 字段
- WechatShare:真实链接分享(type=2/3 截图待 Phase 4.F)
- StartLocationHandler:真实定位 → 9 字段(latitude/longitude string, province 小写 p)

生命周期:
- AppDelegate.didFinishLaunchingWithOptions:WeChatSDK.register + AMapWrapper.bootstrap
- SceneDelegate.openURLContexts:WeChatSDK.handleOpenURL 接入回调
- BackGameDataHandler:子游戏 backgameData pop 时 WXApi.delegate=nil + LocationService.stop()

Info.plist:CFBundleURLTypes 加 wx586a9b321e56efb7;LSApplicationQueriesSchemes 追加
weixin/weixinULAPI/weixinURLParamsAPI;NSLocationWhenInUseUsageDescription +
NSMicrophoneUsageDescription(gamehallname 中文文案)。

.gitignore:排除 docs/res/{AMap_iOS_Loc_ALL,objc-sdk-8.9.2}/(235MB 项目方下载副本,
已走 SPM/Vendor 接入不需要副本)。

文档:docs/SDK-Integration-Guide.md 重写 §A 用户手动 Xcode UI 三步(SPM × 2 +
Add Files × 2);Vendor/{AMap,WechatSDK}/README.md 记录各自接入路径选型。

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

17 KiB
Raw Blame History

SDK 接入指南

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

核心前提:本项目复用 daoqi(msext) 原项目的全部 SDK 凭证。Bundle ID 必须保持 com.skyapp.ylgamehall(已确认与 msext Release 配置一致)— 改 Bundle ID 会导致所有 key 失效。

代码状态(2026-06-22:所有 Swift wrapper / handler 真实路径 / 生命周期清理钩子已落地,全部包在 #if canImport(<ModuleName>) 守卫里。只需用户在 Xcode UI 完成 3 件事(详见 §A 用户手动步骤),代码即激活真实路径。


A. 用户手动 Xcode 步骤(必做 3 件事)

选型偏好:尽量 SPM 远程依赖;只有官方未提供 SPM 的才走 Vendor 二进制。 当前结果:微信 + 七牛走 SPMpbxproj 仅多 2 个 XCRemoteSwiftPackageReference,与现有 ZIPFoundation 一致);高德走 Vendor(官方未提供 SPM)。 这 3 件事都是 Xcode UI 操作,无法 AI 自动完成。完成后 BuildProject 即激活所有真实路径。

A.1 添加微信 OpenSDKSPM 远程依赖)

项目方下载的 WechatOpenSDK-NoPay.xcframework 已撤出 Vendor/;改用 Tencent 官方 SPM 仓库。

  1. Xcode → File → Add Package Dependencies...
  2. 右上搜索框粘贴:https://github.com/Tencent/WechatOpenSDK-XCFramework
  3. Dependency RuleUp to Next Major Version2.0.5(或更新;避免 1.x 的 ITMS-90809 历史包袱)
  4. Add Package → 弹窗里 Library 选 WechatOpenSDK → Add Package
  5. 确认 ylgamehall target 已勾选
  6. ⌘B 编译

A.2 添加高德定位 framework × 2Vendor 手动,无 SPM 替代)

⚠️ 高德官方至今未提供 SPM 包;只发布旧式 fat .framework(不是 xcframework),同时含 x86_64 + arm64 — 模拟器和真机都能用,但要 Do Not Embed(静态库不能 Embed,否则链接器报 __OBJC 段重复符号)。

  1. 选中 ylgamehall Target → General → Frameworks, Libraries, and Embedded Content
  2. + → Add Other → Add Files
  3. 一次性选两个:
    • Vendor/AMap/AMapFoundationKit.framework
    • Vendor/AMap/AMapLocationKit.framework
  4. 两个的 Embed 列都选 Do Not Embed(关键,别错)
  5. ⌘B 编译

A.3 添加七牛 SPM 依赖(远程)

项目方下载的 docs/res/objc-sdk-8.9.2/ 改 SPM 后不再使用,可保留作离线备份或直接删除(不影响工程)。

  1. Xcode → File → Add Package Dependencies...
  2. 右上搜索框粘贴:https://github.com/qiniu/objc-sdk
  3. Dependency RuleUp to Next Major Version8.9.0
  4. Add Package → 弹窗里 Library 选 Qiniu → Add Package
  5. ⌘B 编译

B. 完成 A 三步后会发生什么

代码侧已经全部写好,并用 #if canImport(...) 守卫围着真实路径。一旦 Xcode UI 把 SDK 加进 TargetcanImport 立刻返回 true,下面这些就会自动激活:

文件 激活后行为
AppDelegate.swift WeChatSDK.register() → 真实调 WXApi.registerApp("wx586a9b321e56efb7", universalLink: "")
AppDelegate.swift AMapWrapper.bootstrap() → 真实调隐私合规 3 步 + apiKey
SceneDelegate.swift openURLContexts → 真实调 WXApi.handleOpen(url, delegate:)
AccreditLoginHandler accreditlogin → 真实拉起微信授权 → 反向 callback sharelogin 7 字段(Province 大写 P
WechatShare share() → 真实调 WXApi.send(SendMessageToWXReq)
StartLocationHandler startlocation → 真实调 AMapLocationManager.requestLocation → 反向 callback getlocationinfo 9 字段(latitude/longitude string, province 小写 p
BackGameDataHandler 子游戏 backgameData 退出时自动清 WXApi.delegate = nil + LocationService.stop()

Qiniu 单独说明:七牛 SDK 加进 SPM 后,canImport(QiniuSDK) 通过;但 RemoteAudioHandler 仍然是 stubprepareaudio 不真实录音上传),原因是录音→AMR 转码链路的 opencore-amr ObjC 库未接入。QiniuUploader.upload(...) 单独已可用,可用于其它非录音场景。完整录音上传链路见 §C。


C. 仍需用户手动做的事(opencore-amr / AMR 录音上传)

这一块代码侧没有自动完成,因为需要从 msext 拷贝 ObjC 源码(含 MRC+ 修改 Build SettingsFile-level -fno-objc-arc flag+ Bridging Header。这些 pbxproj 改动对 Xcode 26 同步项目风险较高,留用户手动。

C.1 拷贝 opencore-amr 静态库

mkdir -p ylgamehall/Vendor/opencore-amr/
# 已修复版(fat header segalign 8)静态库
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/
# 拷 VoiceConverter ObjC wrapper(含 amrFileCodec / interf_*
cp -r ../daoqi/msext/msext/Class/Common/VoiceConvert \
      ylgamehall/Vendor/opencore-amr/VoiceConverter

C.2 Xcode 工程配置

  1. 选中 Target → Build Phases → Link Binary With Libraries → + → Add Other → Add Files → 选两个 .a
  2. Target → Build Settings
    • Library Search Paths$(PROJECT_DIR)/Vendor/opencore-amr
    • Header Search Paths$(PROJECT_DIR)/Vendor/opencore-amr/include
  3. Vendor/opencore-amr/VoiceConverter/ 下的 .m 文件拖入项目(Add to Target ylgamehall
  4. 每个 .m 文件单独加 -fno-objc-arc 编译标志(msext 是 MRC):
    • Target → Build Phases → Compile Sources → 找到每个 .m → 双击 → 加 -fno-objc-arc
  5. 创建 Bridging Header ylgamehall-Bridging-Header.h 内容:
    #import "VoiceConverter.h"
    
  6. Target → Build Settings → Objective-C Bridging Header 设为 ylgamehall/ylgamehall-Bridging-Header.h

C.3 接入 Swift 录音 + 上传链路(代码侧)

完成 C.1-C.2 后回到代码侧补两个文件:

Source/Audio/VoiceCoder.swiftSwift wrapper

import Foundation

public enum VoiceCoder {
    public static func wavToAmr(_ wav: URL) throws -> URL {
        let amr = wav.deletingPathExtension().appendingPathExtension("amr")
        // VoiceConverter 是 msext ObjC 类(MRC),桥到 Swift 后直接调
        let ok = VoiceConverter.convertWav(toAmr: wav.path, amrSavePath: amr.path)
        guard ok else { throw VoiceCodeError.transcodeFailed }
        return amr
    }
    public static func amrToWav(_ amr: URL) throws -> URL {
        let wav = amr.deletingPathExtension().appendingPathExtension("wav")
        let ok = VoiceConverter.convertAmr(toWav: amr.path, wavSavePath: wav.path)
        guard ok else { throw VoiceCodeError.transcodeFailed }
        return wav
    }
    public enum VoiceCodeError: Error { case transcodeFailed }
}

Source/Audio/AudioRecorder.swiftactor

import AVFoundation

public actor AudioRecorder {
    public static let shared = AudioRecorder()
    public func record() async throws -> (file: URL, duration: Int) {
        // AVAudioRecorder 16kHz / mono / 16-bit WAV
        // 等价 msext ChatVoiceRecorderVC 配置
        // ... 实现略,约 60 行
    }
    public func cancel() { /* 停录音 */ }
}

升级 RemoteAudioHandler.swift 的 prepareaudio 分支

bridge.register("prepareaudio") { _, callback in
    callback?(.string("Response from prepareaudio"))

    Task { @MainActor in
        do {
            let rec = try await AudioRecorder.shared.record()
            let amr = try VoiceCoder.wavToAmr(rec.file)
            let uploaded = try await QiniuUploader.shared.upload(amr, timeSec: rec.duration)
            bridge.call("getaudiourl", data: .object([
                "audiourl": .string(uploaded.fileUrl),
                "time":     .string("\(uploaded.timeSec)")
            ]), callback: nil)
            // 子游戏页额外触发(仅 SubGameViewController 注册的 handler 时才发,
            // 需要在 SubGameViewController.registerBridgeHandlers 里包一层判别):
            // bridge.call("recordSuccess", data: ...)
        } catch {
            // 失败兜底(msext 同款乐观策略:不反向通知 H5)
        }
    }
}

升级 BackGameDataHandler 清理钩子:取消那行 AudioRecorder.shared.cancel() 注释。


0. 凭证总表(沿用 msext

全部来自 daoqi/msext 原项目,直接复用,无需重新申请

来源
Bundle ID com.skyapp.ylgamehall daoqi/msext.xcodeproj Release
App 显示名 进贤聚友棋牌 SGDefineInfo.h:103 gamehallname
微信 AppID wx586a9b321e56efb7 SGDefineInfo.h:105 kAuthOpenID
微信 AppSecret b2792724b9565be23e8f5ba548f117cf SGDefineInfo.h:107 Appsecret
微信 AuthScope snsapi_message,snsapi_userinfo,snsapi_friend,snsapi_contact SGDefineInfo.h:104
微信 AuthState wechat_sdk SGDefineInfo.h:106 kAuthState
高德定位 APIKey b0d4a8e3fcbbcc0dd96283b7df6a4494 daoqi/msext/Class/Common/APIKey.h:14
七牛 AccessKey dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm QiniuConfig.m:12
七牛 SecretKey RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq QiniuConfig.m:13
七牛 BucketName iosaudio QiniuConfig.m:16
七牛 CDN Domain iosaudio.daoqi88.cn daoqi/msext/msext/qiniudomain/ 目录名

0.1 安全权衡(与父项目 CLAUDE.md 同步)

父项目 CLAUDE.md 第 6 节明确指出 AppSecret / SecretKey 客户端硬编码是已识别但接受不修的安全风险。msext IPA 已分发数年等价泄露,新外壳沿用同一 key + 沿用同款客户端直拼路径 → 同等安全等级不引入新的攻击面不需要后台中转接口


1. 微信 OpenSDKPhase 4.E)— 代码已就绪

1.1 凭证(已具备,沿用 msext)

参 §0 总表。

1.2 工程接入

参 §A.1(用户手动 Xcode 步骤)。

1.3 代码已落地

文件 状态 说明
SPM WechatOpenSDK-XCFramework 待用户在 Xcode Add Package Dependencies 见 §A.1Tencent 官方仓库
Source/SDK/WeChat/WeChatSDK.swift 已写 register() + handleOpenURL 包 canImport
Source/SDK/WeChat/WeChatManager.swift 已写 authorize / shareLink async wrapper + WXApiDelegate
Source/Login/WeChatAuth.swift 已写 OAuth2 客户端直拼 sns/oauth2 + userinfo + 7 字段
Source/Bridge/Handlers/AccreditLoginHandler.swift 已升级 真实拉起授权 + sharelogin 反向 callbackProvince 大写 P
Source/Share/WechatShare.swift 已升级 真实链接分享(type=2/3 截图待 Phase 4.F
Source/Bridge/Handlers/BackGameDataHandler.swift 已加钩子 WXApi.delegate = nil 子游戏 pop 时清理
AppDelegate.swift 已加注册 WeChatSDK.register()
SceneDelegate.swift 已加 openURL WeChatSDK.handleOpenURL(ctx.url)
Info.plist 已加 CFBundleURLTypes + LSApplicationQueriesSchemes 微信 3 项

1.4 验收(用户 Xcode 加完 framework 后跑)

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

1.5 风险

  • AppSecret / SecretKey 已永久泄露(msext IPA 已分发数年)。复用同一 secret 安全等级不变,不引入新风险。
  • 企业签 / TF 分发,不上 App Store,不需要 Universal Links / 审核合规。

2. 高德定位 SDK(Phase 5)— 代码已就绪

2.1 凭证(已具备)

参 §0 总表。APIKey 与 Bundle ID com.skyapp.ylgamehall 绑死,已复用。

2.2 工程接入

参 §A.2(用户手动 Xcode 步骤)。

2.3 代码已落地

文件 状态
Vendor/AMap/AMapFoundationKit.framework + AMapLocationKit.framework 已就位
Source/SDK/AMap/AMapWrapper.swift 已写
Source/Location/LocationService.swift 已写
Source/Bridge/Handlers/StartLocationHandler.swift 已升级
Source/Bridge/Handlers/BackGameDataHandler.swift 已加钩子
AppDelegate.swift 已加注册
Info.plist 已加

2.4 验收

  • 首次启动 → 系统权限弹框(中文文案)
  • H5 调 startlocation → 收 getlocationinfo({...9 字段...})latitude/longitude 是 string、province 小写 p
  • 拒绝权限 → H5 收 getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})
  • 子游戏定位中 → 退出子游戏 → 大厅不再收悬挂定位回调

2.5 风险

  • APIKey 绑定 Bundle ID:本项目已对齐 com.skyapp.ylgamehall,可复用。改 Bundle ID 必须重新申请 key
  • 模拟器定位精度低:必须真机测

3. opencore-amr 转码库(Phase 3.B)— 用户手动

参 §C(用户手动步骤)。代码侧的 wrapper 文件待用户完成 C.1-C.2 后自行新增(§C.3 已给出代码骨架)。


4. 七牛云上传(Phase 3.C / 3.D)— 代码已就绪(除录音链路)

4.1 凭证(已具备)

参 §0 总表。客户端用 AccessKey + SecretKey 本地 HMAC-SHA1 拼 token(与 msext 等价路径),无后台依赖

4.2 工程接入

参 §A.3(用户手动添加 SPM 依赖)。

4.3 代码已落地

文件 状态
Source/Network/QiniuConfig.swift 已写
Source/Network/QiniuTokenSigner.swift 已写
Source/Network/QiniuUploader.swift 已写

4.4 上层调用待 §C 完成

Source/Bridge/Handlers/RemoteAudioHandler.swiftprepareaudio 真实链路依赖 §C 的 AMR 转码 + AudioRecorder。完成后取消 §C.3 的 TODO 注释即可激活全链路。

4.5 验收(依赖 §C 完成)

  • H5 调 prepareaudio → 录音 3 秒 → 上传 → 大厅 H5 收 getaudiourl({audiourl:"http://iosaudio.daoqi88.cn/xxx.amr", time:"3"})
  • 浏览器粘贴 audiourl 能直接下载 amr 文件
  • 退出子游戏 → 进行中上传任务被 cancelSwift Task.cancel 传播)

5. 接入完毕的清单检查

每接入完一个 SDK 跑一遍:

  • BuildProject 成功
  • Vendor/<SDK>/README.md 已记录版本号 / 来源 / 凭证沿用 msext 的备注 — 已就位
  • Info.plist 改动只在原生侧,未触 H5 任何文件 — 已就位
  • BackGameDataHandler.swift 对应清理钩子已加 — 微信 + 高德已加 canImport 守卫;录音待 §C
  • docs/Development-Plan.md §5 / §8 进度勾选已同步
  • docs/Verification-Checklist.md 对应 Phase 验证项已补充
  • 真机跑过验收项(模拟器对微信 / 高德 / AVAudioRecorder 都不可靠)
  • Bundle ID 没改com.skyapp.ylgamehall),改了所有 key 失效

6. 与原项目 daoqi 的关系

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

  • 微信 OAuth + 7 字段:daoqi/msext/Class/RootVC/NewRootVC.m:2428sharelogin 7 字段,含 Province 大写)
  • 微信 SDK 12 个 MMAPP flagdaoqi/msext/AppDelegate.mregisterApp
  • 高德 9 字段:daoqi/msext/Class/RootVC/gameController.m:2528 / 2557getlocationinfo 字段顺序与类型)
  • AMR 转码:daoqi/msext/Class/Common/VoiceConvert/VoiceConverter.h/mamrFileCodec.h/m
  • 七牛 token 自签算法:daoqi/msext/Class/Utils/QiniuManager.m:200-230putPolicy + HMAC-SHA1 + Base64URL
  • 七牛域名读取:daoqi/msext/AppDelegate.m:142FuncPublic filename:@"qiniudomain"Resources/qiniudomain/ 下第一个子目录名作为实际 Domain