feat: 增加门店身份注销流程

This commit is contained in:
2026-08-31 09:43:14 +08:00
parent 396597a160
commit e529bb5942
35 changed files with 6062 additions and 45 deletions
+28 -13
View File
@@ -19,6 +19,7 @@ final class APIClient {
private let session: URLSessionProtocol
private let encoder: JSONEncoder
private let decoder: JSONDecoder
private let notificationCenter: NotificationCenter
private var authTokenProvider: (() -> String?)?
private let environment: APIEnvironment
@@ -32,12 +33,14 @@ final class APIClient {
encoder: JSONEncoder = JSONEncoder(),
decoder: JSONDecoder = JSONDecoder(),
appVersion: String = AppClientInfo.appVersion(),
osType: String = AppClientInfo.osType
osType: String = AppClientInfo.osType,
notificationCenter: NotificationCenter = .default
) {
self.environment = environment
self.session = session
self.encoder = encoder
self.decoder = decoder
self.notificationCenter = notificationCenter
self.appVersion = appVersion.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "1.0.0"
self.osType = osType.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? AppClientInfo.osType
}
@@ -61,7 +64,7 @@ final class APIClient {
tokenOverride: String? = nil
) async throws -> Response {
let request = try makeURLRequest(apiRequest, tokenOverride: tokenOverride)
logRequest(request)
logRequest(request, includeBody: apiRequest.logsPayload)
let data: Data
let response: URLResponse
@@ -80,12 +83,12 @@ final class APIClient {
throw APIError.networkFailed(error.localizedDescription)
}
logResponse(for: request, response: response, data: data)
logResponse(for: request, response: response, data: data, includeBody: apiRequest.logsPayload)
do {
try validateHTTPResponse(response, data: data)
return try decodeEnvelope(Response.self, from: data)
} catch let error as APIError {
notifySessionExpiredIfNeeded(for: error)
notifySessionErrorIfNeeded(for: error, request: request)
throw error
}
}
@@ -141,7 +144,7 @@ final class APIClient {
try validateHTTPResponse(response, data: data)
return try decodeEnvelope(Response.self, from: data)
} catch let error as APIError {
notifySessionExpiredIfNeeded(for: error)
notifySessionErrorIfNeeded(for: error, request: request)
throw error
}
}
@@ -212,7 +215,7 @@ final class APIClient {
try validateHTTPResponse(response, data: responseData)
return try decodeEnvelope(Response.self, from: responseData)
} catch let error as APIError {
notifySessionExpiredIfNeeded(for: error)
notifySessionErrorIfNeeded(for: error, request: request)
throw error
}
}
@@ -267,6 +270,9 @@ final class APIClient {
}
guard 200 ..< 300 ~= httpResponse.statusCode else {
if let envelope = try? decoder.decode(ErrorEnvelope.self, from: data), envelope.code == 150015 {
throw APIError.serverCode(150015, parseHTTPErrorMessage(data: data))
}
throw APIError.httpStatus(httpResponse.statusCode, parseHTTPErrorMessage(data: data))
}
}
@@ -295,10 +301,19 @@ final class APIClient {
return payload
}
/// Token 失效时广播 sessionDidExpire,触发全局登出。
private func notifySessionExpiredIfNeeded(for error: APIError) {
/// 区分身份注销受限与凭证失效;150015 附带原请求 Token,便于忽略切换身份后的旧错误。
private func notifySessionErrorIfNeeded(for error: APIError, request: URLRequest) {
if case .serverCode(150015, _) = error {
guard let token = request.value(forHTTPHeaderField: "token"), !token.isEmpty else { return }
notificationCenter.post(
name: NotificationName.storeAccountDeregistrationRestricted,
object: nil,
userInfo: [NotificationUserInfoKey.deregistrationRequestToken: token]
)
return
}
guard APIError.isAuthenticationExpired(error) else { return }
NotificationCenter.default.post(name: NotificationName.sessionDidExpire, object: nil)
notificationCenter.post(name: NotificationName.sessionDidExpire, object: nil)
}
/// 从 HTTP 错误响应中提取更适合展示给用户的错误信息。
@@ -341,7 +356,7 @@ final class APIClient {
}
/// 在 Debug 环境打印请求信息(含 GET 查询参数与 POST 请求体,对齐 Android Ktor `LogLevel.BODY`)。
private func logRequest(_ request: URLRequest) {
private func logRequest(_ request: URLRequest, includeBody: Bool = true) {
#if DEBUG
let method = request.httpMethod ?? "REQUEST"
let url = request.url?.absoluteString ?? "<invalid url>"
@@ -359,7 +374,7 @@ final class APIClient {
}
let contentType = request.value(forHTTPHeaderField: "Content-Type")
if let body = Self.debugRequestBody(from: request.httpBody, contentType: contentType) {
if includeBody, let body = Self.debugRequestBody(from: request.httpBody, contentType: contentType) {
lines.append("body:\n\(body)")
}
@@ -368,12 +383,12 @@ final class APIClient {
}
/// 在 Debug 环境打印响应状态和响应体。
private func logResponse(for request: URLRequest, response: URLResponse, data: Data) {
private func logResponse(for request: URLRequest, response: URLResponse, data: Data, includeBody: Bool = true) {
#if DEBUG
let method = request.httpMethod ?? "REQUEST"
let url = request.url?.absoluteString ?? "<invalid url>"
let statusCode = (response as? HTTPURLResponse).map { String($0.statusCode) } ?? "unknown"
let body = Self.debugResponseBody(from: data)
let body = includeBody ? Self.debugResponseBody(from: data) : "<sensitive payload omitted>"
print("[API][Response] \(method) \(url) status=\(statusCode)\n\(body)")
#endif
}
+5 -1
View File
@@ -20,6 +20,8 @@ nonisolated struct APIRequest<Response: Decodable> {
var queryItems: [URLQueryItem]
var headers: [String: String]
var body: AnyEncodable?
/// 敏感请求关闭正文日志,避免验证码和注销资产进入调试日志。
var logsPayload: Bool
/// 创建一个 API 请求,并把可编码请求体擦除为统一的 AnyEncodable。
init<Body: Encodable>(
@@ -27,13 +29,15 @@ nonisolated struct APIRequest<Response: Decodable> {
path: String,
queryItems: [URLQueryItem] = [],
headers: [String: String] = [:],
body: Body? = Optional<EmptyPayload>.none
body: Body? = Optional<EmptyPayload>.none,
logsPayload: Bool = true
) {
self.method = method
self.path = path
self.queryItems = queryItems
self.headers = headers
self.body = body.map(AnyEncodable.init)
self.logsPayload = logsPayload
}
}