Files
youle_app_ios_v2/ylgamehall/Source/Login/WeChatAuth.swift
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

129 lines
5.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// WeChatAuth.swift
// ylgamehall
//
// 微信 OAuth2 客户端直拼实现(与 msext 4 处 ViewController 同款路径)。
// Code → access_token → userinfo → 7 字段 user。
//
// ⚠️ AppSecret 客户端硬编码 = 父项目 CLAUDE.md 已识别但接受的安全风险。
// msext IPA 已分发数年,Appsecret 等价泄露;沿用同一 secret 不引入新攻击面,
// 也无需后台 /wechat/login 中转。详见 docs/SDK-Integration-Guide.md §0.1。
//
// 字段名严格 1:1(与 msext NewRootVC.m:2428 sharelogin payload 等价):
// openid / headimgurl / nickname / sex / city / Province(大写 P/ unionid
//
import Foundation
public struct WeChatUser: Sendable {
public let openid: String
public let headimgurl: String
public let nickname: String
/// 性别。微信回的是 number(1=男 2=女 0=未知);H5 收到的 sharelogin 是同款 number 字面。
public let sex: Int
/// city / province 经 msext `danbian:` 去单引号处理(防 H5 JSON 解析炸)
public let city: String
/// 注意大写 Pmsext NewRootVC.m:2428 payload key 是 "Province",硬约束
public let Province: String
public let unionid: String
}
public enum WeChatAuthError: Error, Sendable {
case userCancelled
case authStepFailed(Int)
case tokenRequestFailed(any Error)
case tokenResponseInvalid(String)
case userInfoRequestFailed(any Error)
case userInfoResponseInvalid(String)
}
@MainActor
public enum WeChatAuth {
/// 微信 AppSecret(沿用 msext SGDefineInfo.h:107)— 客户端直拼 sns/oauth2 用
static let appSecret = "b2792724b9565be23e8f5ba548f117cf"
/// 完整 OAuth2 流程:拉起授权 → 拿 code → 换 access_token → 拿 userinfo → 返回 7 字段 user
public static func authorize() async throws -> WeChatUser {
// Step 1: 拿 code
let payload: WXAuthCodePayload
do {
payload = try await WeChatManager.shared.authorize()
} catch let WeChatError.authFailed(errCode) {
// msext 行为:errCode == -2 视为用户取消,不弹错误
if errCode == -2 { throw WeChatAuthError.userCancelled }
throw WeChatAuthError.authStepFailed(errCode)
}
// Step 2: code → access_token + openid
let token = try await exchangeAccessToken(code: payload.code)
// Step 3: access_token + openid → userinfo7 字段)
return try await fetchUserInfo(accessToken: token.accessToken, openid: token.openid)
}
// MARK: - Private — OAuth2 step 2/3 directly hitting api.weixin.qq.com
private struct TokenResponse {
let accessToken: String
let openid: String
}
private static func exchangeAccessToken(code: String) async throws -> TokenResponse {
let urlString = "https://api.weixin.qq.com/sns/oauth2/access_token"
+ "?appid=\(WeChatSDK.appID)"
+ "&secret=\(appSecret)"
+ "&code=\(code)"
+ "&grant_type=authorization_code"
guard let url = URL(string: urlString) else {
throw WeChatAuthError.tokenResponseInvalid("bad url")
}
let data: Data
do {
(data, _) = try await URLSession.shared.data(from: url)
} catch {
throw WeChatAuthError.tokenRequestFailed(error)
}
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
let accessToken = obj["access_token"] as? String,
let openid = obj["openid"] as? String
else {
let raw = String(data: data, encoding: .utf8) ?? "<binary>"
throw WeChatAuthError.tokenResponseInvalid(raw)
}
return TokenResponse(accessToken: accessToken, openid: openid)
}
private static func fetchUserInfo(accessToken: String, openid: String) async throws -> WeChatUser {
let urlString = "https://api.weixin.qq.com/sns/userinfo"
+ "?access_token=\(accessToken)"
+ "&openid=\(openid)"
guard let url = URL(string: urlString) else {
throw WeChatAuthError.userInfoResponseInvalid("bad url")
}
let data: Data
do {
(data, _) = try await URLSession.shared.data(from: url)
} catch {
throw WeChatAuthError.userInfoRequestFailed(error)
}
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
let raw = String(data: data, encoding: .utf8) ?? "<binary>"
throw WeChatAuthError.userInfoResponseInvalid(raw)
}
return WeChatUser(
openid: (obj["openid"] as? String) ?? "",
headimgurl: (obj["headimgurl"] as? String) ?? "",
nickname: (obj["nickname"] as? String) ?? "",
sex: (obj["sex"] as? Int) ?? 0,
// msext danbian: 去单引号(防 JSON 序列化炸)
city: danbian((obj["city"] as? String) ?? ""),
Province: danbian((obj["province"] as? String) ?? ""),
unionid: (obj["unionid"] as? String) ?? ""
)
}
/// msext FuncPublic.danbian: 等价:去掉单引号
private static func danbian(_ s: String) -> String {
s.replacingOccurrences(of: "'", with: "")
}
}