docs(sdk): 添加 Android SDK 文档和 API 设计规范
- 新增 Android SDK 使用文档,包含模块结构、集成方式和快速开始指南 - 添加 SDK API 重设计规范,统一初始化和登录接口设计 - 补充安全设计规范,完善 UserSig 鉴权和敏感数据处理方案 - 创建平台 REST API 规范,定义服务端到服务端的调用接口 - 添加离线推送架构设计,集成各大厂商推送服务与 IM 联动方案
这个提交包含在:
父节点
b413573178
当前提交
2352b46c6b
12
README.md
12
README.md
@ -68,8 +68,6 @@ struct MyApp: App {
|
|||||||
XuqmSDK.initialize(
|
XuqmSDK.initialize(
|
||||||
appKey: "ak_your_app_key",
|
appKey: "ak_your_app_key",
|
||||||
appSecret: "as_your_app_secret",
|
appSecret: "as_your_app_secret",
|
||||||
apiBaseUrl: "https://api.xuqm.com",
|
|
||||||
imBaseUrl: "wss://im.xuqm.com",
|
|
||||||
debug: true
|
debug: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -100,7 +98,7 @@ struct LoginResponse: Decodable {
|
|||||||
// 发起请求
|
// 发起请求
|
||||||
let response: LoginResponse = try await ApiClient.post(
|
let response: LoginResponse = try await ApiClient.post(
|
||||||
path: "/api/im/auth/login",
|
path: "/api/im/auth/login",
|
||||||
body: ["appId": appId, "userId": userId]
|
body: ["appKey": appKey, "userId": userId]
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -197,10 +195,10 @@ import XuqmSDK
|
|||||||
func application(_ application: UIApplication,
|
func application(_ application: UIApplication,
|
||||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||||
Task {
|
Task {
|
||||||
|
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
|
||||||
try await PushSDK.registerToken(
|
try await PushSDK.registerToken(
|
||||||
appId: "ak_xxx",
|
token,
|
||||||
userId: "user_001",
|
userId: "user_001"
|
||||||
token: deviceToken
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,7 +215,7 @@ func application(_ application: UIApplication,
|
|||||||
```swift
|
```swift
|
||||||
import XuqmSDK
|
import XuqmSDK
|
||||||
|
|
||||||
let result = try await UpdateSDK.checkAppUpdate(appId: "ak_xxx")
|
let result = try await UpdateSDK.checkAppUpdate(currentVersionCode: 123)
|
||||||
if result.needsUpdate, let info = result.info {
|
if result.needsUpdate, let info = result.info {
|
||||||
print("最新版本: \(info.versionName)")
|
print("最新版本: \(info.versionName)")
|
||||||
// iOS 只能跳转 App Store
|
// iOS 只能跳转 App Store
|
||||||
|
|||||||
@ -32,7 +32,7 @@ public final class ApiClient: @unchecked Sendable {
|
|||||||
guard let config else { throw URLError(.badURL) }
|
guard let config else { throw URLError(.badURL) }
|
||||||
let tokenStore = await XuqmSDK.shared.tokenStore
|
let tokenStore = await XuqmSDK.shared.tokenStore
|
||||||
|
|
||||||
var components = URLComponents(url: config.apiBaseURL.appendingPathComponent(path), resolvingAgainstBaseURL: false)!
|
var components = URLComponents(url: SDKEndpoints.apiBaseURL.appendingPathComponent(path), resolvingAgainstBaseURL: false)!
|
||||||
if let qi = queryItems { components.queryItems = qi }
|
if let qi = queryItems { components.queryItems = qi }
|
||||||
|
|
||||||
var req = URLRequest(url: components.url!)
|
var req = URLRequest(url: components.url!)
|
||||||
|
|||||||
@ -1,44 +1,40 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct SDKConfig: Sendable {
|
public struct SDKConfig: Sendable {
|
||||||
public let appId: String
|
|
||||||
public let appKey: String
|
public let appKey: String
|
||||||
public let appSecret: String
|
public let appSecret: String
|
||||||
public let apiBaseURL: URL
|
|
||||||
public let imWebSocketURL: URL
|
|
||||||
public let debug: Bool
|
public let debug: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
appId: String,
|
|
||||||
appKey: String,
|
appKey: String,
|
||||||
appSecret: String,
|
appSecret: String,
|
||||||
apiBaseURL: URL,
|
|
||||||
imWebSocketURL: URL,
|
|
||||||
debug: Bool = false
|
debug: Bool = false
|
||||||
) {
|
) {
|
||||||
self.appId = appId
|
|
||||||
self.appKey = appKey
|
self.appKey = appKey
|
||||||
self.appSecret = appSecret
|
self.appSecret = appSecret
|
||||||
self.apiBaseURL = apiBaseURL
|
|
||||||
self.imWebSocketURL = imWebSocketURL
|
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension SDKConfig {
|
public extension SDKConfig {
|
||||||
static func development(
|
static func development(
|
||||||
appId: String,
|
appKey: String,
|
||||||
appSecret: String,
|
appSecret: String,
|
||||||
appKey: String? = nil,
|
|
||||||
debug: Bool = false
|
debug: Bool = false
|
||||||
) -> SDKConfig {
|
) -> SDKConfig {
|
||||||
SDKConfig(
|
SDKConfig(
|
||||||
appId: appId,
|
appKey: appKey,
|
||||||
appKey: appKey ?? appId,
|
|
||||||
appSecret: appSecret,
|
appSecret: appSecret,
|
||||||
apiBaseURL: URL(string: "http://192.168.116.9:8081")!,
|
|
||||||
imWebSocketURL: URL(string: "ws://192.168.116.9:8082/ws/im")!,
|
|
||||||
debug: debug
|
debug: debug
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SDKConfig {
|
||||||
|
var appId: String { appKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SDKEndpoints {
|
||||||
|
static let apiBaseURL = URL(string: "https://dev.xuqinmin.com")!
|
||||||
|
static let imWebSocketURL = URL(string: "wss://dev.xuqinmin.com/ws/im")!
|
||||||
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
private let subscriptionId = "sub-user-queue"
|
private let subscriptionId = "sub-user-queue"
|
||||||
private var groupSubscriptions = Set<String>()
|
private var groupSubscriptions = Set<String>()
|
||||||
|
|
||||||
private let wsURLOverride: URL?
|
|
||||||
private let tokenOverride: String?
|
private let tokenOverride: String?
|
||||||
private let appIdOverride: String?
|
private let appIdOverride: String?
|
||||||
|
|
||||||
@ -21,8 +20,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
private var activeAppId: String?
|
private var activeAppId: String?
|
||||||
private var activeUserId: String?
|
private var activeUserId: String?
|
||||||
|
|
||||||
public init(wsURL: URL? = nil, token: String? = nil, appId: String? = nil) {
|
public init(token: String? = nil, appId: String? = nil) {
|
||||||
self.wsURLOverride = wsURL
|
|
||||||
self.tokenOverride = token
|
self.tokenOverride = token
|
||||||
self.appIdOverride = appId
|
self.appIdOverride = appId
|
||||||
super.init()
|
super.init()
|
||||||
@ -37,7 +35,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
reconnectWorkItem?.cancel()
|
reconnectWorkItem?.cancel()
|
||||||
reconnectWorkItem = nil
|
reconnectWorkItem = nil
|
||||||
|
|
||||||
activeWsURL = wsURLOverride
|
activeWsURL = SDKEndpoints.imWebSocketURL
|
||||||
activeToken = tokenOverride
|
activeToken = tokenOverride
|
||||||
activeAppId = appIdOverride
|
activeAppId = appIdOverride
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ public final class ImSDK {
|
|||||||
currentUserId = userId
|
currentUserId = userId
|
||||||
XuqmSDK.shared.tokenStore?.save(res.token)
|
XuqmSDK.shared.tokenStore?.save(res.token)
|
||||||
client?.disconnect()
|
client?.disconnect()
|
||||||
client = ImClient(wsURL: config.imWebSocketURL, token: res.token, appId: config.appId)
|
client = ImClient(token: res.token, appId: config.appId)
|
||||||
client?.setCurrentUserId(userId)
|
client?.setCurrentUserId(userId)
|
||||||
client?.delegate = delegate
|
client?.delegate = delegate
|
||||||
client?.connect()
|
client?.connect()
|
||||||
|
|||||||
@ -6,13 +6,14 @@
|
|||||||
#
|
#
|
||||||
# Config: add a .env.xuqm file to your project root with:
|
# Config: add a .env.xuqm file to your project root with:
|
||||||
# XUQM_SERVER_URL=https://update.dev.xuqinmin.com
|
# XUQM_SERVER_URL=https://update.dev.xuqinmin.com
|
||||||
# XUQM_APP_ID=your-app-id
|
# XUQM_APP_KEY=your-app-key
|
||||||
# XUQM_API_TOKEN=your-api-token
|
# XUQM_API_TOKEN=your-api-token
|
||||||
# XUQM_SCHEME=MyApp
|
# XUQM_SCHEME=MyApp
|
||||||
# XUQM_WORKSPACE=MyApp.xcworkspace
|
# XUQM_WORKSPACE=MyApp.xcworkspace
|
||||||
# XUQM_BUNDLE_ID=com.example.myapp
|
# XUQM_BUNDLE_ID=com.example.myapp
|
||||||
# XUQM_STORE_TARGETS=APP_STORE # comma-separated, e.g. "APP_STORE"
|
# XUQM_STORE_TARGETS=APP_STORE # comma-separated, e.g. "APP_STORE"
|
||||||
# XUQM_AUTO_PUBLISH_AFTER_REVIEW=false
|
# XUQM_AUTO_PUBLISH_AFTER_REVIEW=false
|
||||||
|
# XUQM_PUBLISH_IMMEDIATELY=false
|
||||||
# XUQM_WEBHOOK_URL= # optional
|
# XUQM_WEBHOOK_URL= # optional
|
||||||
# XUQM_SCHEDULED_PUBLISH_AT= # optional, ISO datetime
|
# XUQM_SCHEDULED_PUBLISH_AT= # optional, ISO datetime
|
||||||
# # For App Store Connect API (used by upload_to_app_store):
|
# # For App Store Connect API (used by upload_to_app_store):
|
||||||
@ -31,23 +32,24 @@ platform :ios do
|
|||||||
desc "Build, upload to XuqmGroup update service, and submit to App Store"
|
desc "Build, upload to XuqmGroup update service, and submit to App Store"
|
||||||
lane :xuqm_release do
|
lane :xuqm_release do
|
||||||
server_url = ENV.fetch("XUQM_SERVER_URL")
|
server_url = ENV.fetch("XUQM_SERVER_URL")
|
||||||
app_id = ENV.fetch("XUQM_APP_ID")
|
app_key = ENV.fetch("XUQM_APP_KEY")
|
||||||
api_token = ENV.fetch("XUQM_API_TOKEN")
|
api_token = ENV.fetch("XUQM_API_TOKEN")
|
||||||
scheme = ENV.fetch("XUQM_SCHEME")
|
scheme = ENV.fetch("XUQM_SCHEME")
|
||||||
workspace = ENV.fetch("XUQM_WORKSPACE")
|
workspace = ENV.fetch("XUQM_WORKSPACE")
|
||||||
bundle_id = ENV.fetch("XUQM_BUNDLE_ID", "")
|
bundle_id = ENV.fetch("XUQM_BUNDLE_ID", "")
|
||||||
store_targets = ENV.fetch("XUQM_STORE_TARGETS", "")
|
store_targets = ENV.fetch("XUQM_STORE_TARGETS", "")
|
||||||
auto_publish = ENV.fetch("XUQM_AUTO_PUBLISH_AFTER_REVIEW", "false") == "true"
|
auto_publish = ENV.fetch("XUQM_AUTO_PUBLISH_AFTER_REVIEW", "false") == "true"
|
||||||
|
publish_immediately = ENV.fetch("XUQM_PUBLISH_IMMEDIATELY", "false") == "true"
|
||||||
webhook_url = ENV.fetch("XUQM_WEBHOOK_URL", "")
|
webhook_url = ENV.fetch("XUQM_WEBHOOK_URL", "")
|
||||||
scheduled_at = ENV.fetch("XUQM_SCHEDULED_PUBLISH_AT", "")
|
scheduled_at = ENV.fetch("XUQM_SCHEDULED_PUBLISH_AT", "")
|
||||||
|
|
||||||
# ── 1. Read local version ────────────────────────────────────────────
|
# ── 1. Read local version ────────────────────────────────────────────
|
||||||
version_name = get_version_number(target: scheme)
|
version_name = get_version_number(target: scheme)
|
||||||
version_code = get_build_number.to_i
|
version_code = get_build_number.to_i
|
||||||
UI.message("Local version: #{version_name} (#{version_code})")
|
UI.message("Local version: #{version_name} (#{version_code}), appKey: #{app_key}")
|
||||||
|
|
||||||
# ── 2. Check server latest ───────────────────────────────────────────
|
# ── 2. Check server latest ───────────────────────────────────────────
|
||||||
server_version_code = xuqm_get_latest_version_code(server_url, app_id, api_token, "IOS")
|
server_version_code = xuqm_get_latest_version_code(server_url, app_key, api_token, "IOS")
|
||||||
UI.message("Server latest versionCode: #{server_version_code}")
|
UI.message("Server latest versionCode: #{server_version_code}")
|
||||||
|
|
||||||
if version_code <= server_version_code
|
if version_code <= server_version_code
|
||||||
@ -78,7 +80,7 @@ platform :ios do
|
|||||||
change_log = prompt(text: "Release notes: ", ci_input: "")
|
change_log = prompt(text: "Release notes: ", ci_input: "")
|
||||||
version_id = xuqm_upload_ipa(
|
version_id = xuqm_upload_ipa(
|
||||||
server_url: server_url,
|
server_url: server_url,
|
||||||
app_id: app_id,
|
app_key: app_key,
|
||||||
api_token: api_token,
|
api_token: api_token,
|
||||||
ipa_path: ipa_path,
|
ipa_path: ipa_path,
|
||||||
version_name: version_name,
|
version_name: version_name,
|
||||||
@ -134,14 +136,33 @@ platform :ios do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if publish_immediately && !auto_publish && scheduled_at.empty?
|
||||||
|
xuqm_post(
|
||||||
|
url: "#{server_url}/api/v1/updates/app/#{version_id}/publish",
|
||||||
|
token: api_token,
|
||||||
|
body: {}.to_json,
|
||||||
|
)
|
||||||
|
UI.success("Update service version published immediately.")
|
||||||
|
end
|
||||||
|
|
||||||
UI.success("Release complete. Version ID: #{version_id}")
|
UI.success("Release complete. Version ID: #{version_id}")
|
||||||
UI.message(scheduled_at.empty? ? "Publish manually or wait for auto-publish." : "Will publish at #{scheduled_at}")
|
if scheduled_at.empty?
|
||||||
|
if auto_publish
|
||||||
|
UI.message("Will auto-publish after all store reviews pass.")
|
||||||
|
elsif publish_immediately
|
||||||
|
UI.message("Published immediately in update service.")
|
||||||
|
else
|
||||||
|
UI.message("Publish manually or wait for auto-publish.")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
UI.message("Will publish at #{scheduled_at}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ── Private helpers ──────────────────────────────────────────────────────
|
# ── Private helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private_lane :xuqm_get_latest_version_code do |options|
|
private_lane :xuqm_get_latest_version_code do |options|
|
||||||
uri = URI("#{options[:server_url]}/api/v1/updates/app/list?appId=#{options[:app_id]}&platform=#{options[:platform]}")
|
uri = URI("#{options[:server_url]}/api/v1/updates/app/list?appId=#{options[:app_key]}&platform=#{options[:platform]}")
|
||||||
req = Net::HTTP::Get.new(uri)
|
req = Net::HTTP::Get.new(uri)
|
||||||
req["Authorization"] = "Bearer #{options[:api_token]}"
|
req["Authorization"] = "Bearer #{options[:api_token]}"
|
||||||
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(req) }
|
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(req) }
|
||||||
@ -155,7 +176,7 @@ platform :ios do
|
|||||||
|
|
||||||
uri = URI("#{options[:server_url]}/api/v1/updates/app/upload")
|
uri = URI("#{options[:server_url]}/api/v1/updates/app/upload")
|
||||||
request = Net::HTTP::Post::Multipart.new(uri.path,
|
request = Net::HTTP::Post::Multipart.new(uri.path,
|
||||||
"appId" => options[:app_id],
|
"appId" => options[:app_key],
|
||||||
"platform" => "IOS",
|
"platform" => "IOS",
|
||||||
"versionName" => options[:version_name],
|
"versionName" => options[:version_name],
|
||||||
"versionCode" => options[:version_code].to_s,
|
"versionCode" => options[:version_code].to_s,
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户