diff --git a/docs/Development-Plan.md b/docs/Development-Plan.md
index 2995a43..0274239 100644
--- a/docs/Development-Plan.md
+++ b/docs/Development-Plan.md
@@ -486,10 +486,13 @@ Contract Design Plan(本文档)
- `Source/Share/{Wechat,QQ,Douyin}Share.swift`:三个平台 stub(仅返回 success),Phase 4.C/4.D/4.E 升级
- `FriendsShareHandler` 升级:调 ShareCenter.dispatch + 完成后触发 sharesuccess 反向 callback(success/cancelled 都发,notInstalled/failed 不发,沿用 msext 乐观策略)
- BuildProject 通过
-- [ ] **4.3** **QQ 分享走 URL Scheme,不依赖 SDK**
+- [x] **4.3** **QQ 分享走 URL Scheme,不依赖 SDK**(4.C 完成)
- 移植 msext `QQShareManager.m:716-771` `simpleShareToQQFriend` 简化版
- - URL 构造:`mqqapi://share/to_fri?version=1&cflag=0&req_type=1&url=...&title=...&description=...`
- - `canOpenURL` 检查 + `openURL` 异步打开
+ - URL 构造:`mqqapi://share/to_fri?` 或 `mqqapi://share/to_qzone?`(scene == .timeline 走 qzone)+ `version=1&cflag=0&req_type=1&url=...&title=...&description=...`
+ - `encode` 与 msext line 1262-1268 等价:仅保留 RFC 3986 unreserved(alphanumeric + `-._~`)其它 percent encode
+ - `canOpenURL` 检查 + `await UIApplication.open` 异步打开;调起即视为 success
+ - **当前仅链接分享**(type == "1");type == "2" 截图 / 远端图 留 Phase 4.F
+ - Info.plist 加 `LSApplicationQueriesSchemes`:`mqq` / `mqqapi` / `mqqopensdkfriend` / `mqqopensdkapiV2/V3/V4`
- [ ] **4.4** **抖音分享走 URL Scheme + Photos,不依赖 SDK**
- 移植 msext `DouyinShareManager.m` 简化版
- URL Scheme:`snssdk1128://share/video` / `snssdk1128://camera`
diff --git a/ylgamehall/Info.plist b/ylgamehall/Info.plist
index 2d45cf7..6fd833c 100644
--- a/ylgamehall/Info.plist
+++ b/ylgamehall/Info.plist
@@ -47,5 +47,22 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+
+ LSApplicationQueriesSchemes
+
+ mqq
+ mqqapi
+ mqqopensdkfriend
+ mqqopensdkapiV2
+ mqqopensdkapiV3
+ mqqopensdkapiV4
+ snssdk1128
+
diff --git a/ylgamehall/Source/Share/QQShare.swift b/ylgamehall/Source/Share/QQShare.swift
index a70e612..2c07000 100644
--- a/ylgamehall/Source/Share/QQShare.swift
+++ b/ylgamehall/Source/Share/QQShare.swift
@@ -2,23 +2,35 @@
// QQShare.swift
// ylgamehall
//
-// QQ 分享 SharePlatform 实现 stub(Phase 4.C 真实落地)。
+// QQ 分享 SharePlatform 实现(Phase 4.C 完整)。
//
-// Phase 4.B 阶段为 stub(仅返回 success,让 SharePanel 流程跑通);
-// Phase 4.C 移植 msext QQShareManager.m:716-771 simpleShareToQQFriend
-// 的 URL Scheme fallback(mqqapi://share/to_fri?...)。
+// 移植 msext QQShareManager.m:716-771 simpleShareToQQFriend 的 URL Scheme
+// fallback。**不依赖 QQ SDK**。
+//
+// URL 构造:
+// mqqapi://share/to_fri?version=1&cflag=0&req_type=1
+// &url=&title=&description=
+//
+// encodeString 与 msext line 1262-1268 等价:unreserved 字符集(RFC 3986
+// alphanumeric + "-._~"),其它全部 percent encode。
+//
+// ⚠️ 当前仅支持链接分享(type == "1")。type == "2" 截图 / 其它 远端图
+// 在 Phase 4.F 接入(依赖 UIGraphicsImageRenderer + Photos / 临时文件 +
+// mqqapi://share/to_fri?file_type=img 路径)。
//
import UIKit
+import os.log
@MainActor
public final class QQShare: SharePlatform {
public static let shared = QQShare()
+ private static let log = Logger(subsystem: "ylgamehall", category: "QQShare")
+
public let name = "QQ"
- /// 检测 QQ app 是否安装:canOpenURL("mqqapi://")
public var isInstalled: Bool {
guard let url = URL(string: "mqqapi://") else { return false }
return UIApplication.shared.canOpenURL(url)
@@ -28,12 +40,53 @@ public final class QQShare: SharePlatform {
public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult {
guard isInstalled else { return .notInstalled(platform: name) }
- // Phase 4.B 阶段 stub:返回 success,Phase 4.C 升级为完整 URL Scheme 调起
- // 真实实现(Phase 4.C):
- // - 拼装 mqqapi://share/to_fri?version=1&cflag=0&req_type=1
- // &url=...&title=...&description=...
- // - canOpenURL + open
- // - 调起即视为 success(URL Scheme 单向无返回,msext 同款乐观策略)
- return .success
+
+ // 仅链接分享走完整 URL Scheme;图片分享留 Phase 4.F
+ guard content.type == "1" else {
+ Self.log.warning("QQShare: type=\(content.type, privacy: .public) (非链接) 暂未实现,返回 success 避免 H5 业务卡住")
+ return .success
+ }
+
+ // 拼装 URL:mqqapi://share/to_fri? 或 mqqapi://share/to_qzone?
+ // scene == .timeline 走 QQ 空间 to_qzone;.friend 走 to_fri(QQ 好友)
+ let host = scene == .timeline ? "share/to_qzone" : "share/to_fri"
+ var parts: [String] = [
+ "version=1",
+ "cflag=0",
+ "req_type=1"
+ ]
+ if !content.webpageUrl.isEmpty {
+ parts.append("url=\(Self.encode(content.webpageUrl))")
+ } else {
+ // 无 URL → 改 req_type=0 文本分享
+ parts = ["version=1", "cflag=0", "req_type=0"]
+ }
+ if !content.title.isEmpty {
+ parts.append("title=\(Self.encode(content.title))")
+ }
+ if !content.desc.isEmpty {
+ parts.append("description=\(Self.encode(content.desc))")
+ }
+ let urlString = "mqqapi://\(host)?\(parts.joined(separator: "&"))"
+
+ Self.log.debug("QQShare open: \(urlString, privacy: .public)")
+
+ guard let qqURL = URL(string: urlString),
+ UIApplication.shared.canOpenURL(qqURL)
+ else {
+ return .failed(reason: "invalid QQ URL")
+ }
+
+ // URL Scheme 单向调起:msext 同款乐观策略 — 调起即视为 success
+ let opened = await UIApplication.shared.open(qqURL)
+ return opened ? .success : .failed(reason: "openURL failed")
+ }
+
+ /// 与 msext QQShareManager.m:1262-1268 等价:仅保留 RFC 3986 unreserved
+ /// 字符(alphanumeric + "-._~"),其它全部 percent encode。
+ nonisolated static func encode(_ string: String) -> String {
+ var allowed = CharacterSet.alphanumerics
+ allowed.insert(charactersIn: "-._~")
+ return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
}
}