Files
keyboard/keyBoard/Class/Network/KBNetworkManager.m
2025-12-04 20:27:26 +08:00

605 lines
28 KiB
Objective-C
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.

//
// KBNetworkManager.m
// CustomKeyboard
//
#import "KBNetworkManager.h"
#import <TargetConditionals.h>
#import "AFNetworking.h"
#import "KBAuthManager.h"
#import "KBBizCode.h"
// 仅在主 App 内需要的会话管理与 HUD 组件
#import "KBUserSessionManager.h"
#import "KBHUD.h"
#import "KBSignUtils.h"
NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
@interface KBNetworkManager ()
@property (nonatomic, strong) AFHTTPSessionManager *manager; // AFN 管理器ephemeral 配置)
// 私有错误派发
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion;
@end
#if DEBUG
// 在使用到 Debug 辅助方法之前做前置声明,避免出现
// “No visible @interface declares the selector …” 的编译错误。
@interface KBNetworkManager (Debug)
- (NSString *)kb_prettyJSONStringFromObject:(id)obj;
- (NSString *)kb_textFromData:(NSData *)data;
- (NSString *)kb_trimmedString:(NSString *)s maxLength:(NSUInteger)maxLen;
@end
#endif
@implementation KBNetworkManager
+ (instancetype)shared {
static KBNetworkManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBNetworkManager new]; });
return m;
}
- (instancetype)init {
if (self = [super init]) {
_enabled = NO; // 键盘扩展默认无网络能力,需外部显式开启
_timeout = 10.0;
// 默认请求头Accept 任意类型 + 使用项目多语言管理器设置 Accept-Language
NSString *lang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
NSString *token = [KBUserSessionManager shared].accessToken ? [KBUserSessionManager shared].accessToken : @"";
NSString *appId = @"loveKeyboard";
NSString *secret = @"kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H"; // 和服务端保持一致
NSString *timestamp = [KBSignUtils currentTimestamp];
NSString *nonce = [KBSignUtils generateNonceWithLength:16];
// 业务参数body 或 query
NSDictionary *bodyParams = @{
};
// 1. 组装参与签名的所有参数
NSMutableDictionary<NSString *, NSString *> *signParams = [NSMutableDictionary dictionary];
signParams[@"appId"] = appId;
signParams[@"timestamp"] = timestamp;
signParams[@"nonce"] = nonce;
// 如果还有 query 参数也塞进来
// signParams[@"lang"] = @"zh";
// 把 body 里的字段也加入签名参数
[bodyParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([obj isKindOfClass:[NSString class]]) {
signParams[key] = obj;
} else {
signParams[key] = [obj description];
}
}];
// 2. 计算签名
NSString *sign = [KBSignUtils signWithParams:signParams secret:secret];
_defaultHeaders = @{
@"Accept": @"*/*",
@"Accept-Language": lang,
@"auth-token" : token,
@"X-Sign" : sign,
@"X-Timestamp" : timestamp,
@"X-Nonce" : nonce,
@"X-App-Id" : appId
};
// 设置基础域名,路径可相对该地址拼接
_baseURL = [NSURL URLWithString:KB_BASE_URL];
}
return self;
}
#
#pragma mark - Public
// JSON GET可控制业务错误是否由内部弹出
- (NSURLSessionDataTask *)GET:(NSString *)path
parameters:(NSDictionary *)parameters
headers:(NSDictionary<NSString *,NSString *> *)headers
autoShowBusinessError:(BOOL)autoShowBusinessError
completion:(KBNetworkCompletion)completion {
NSLog(@"[KBNetworkManager] GET called, enabled=%d, path=%@", self.isEnabled, path);
if (![self ensureEnabled:completion]) return nil;
NSString *urlString = [self buildURLStringWithPath:path];
if (!urlString) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; }
// 使用 AFHTTPRequestSerializer 生成带参数与头的请求
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
serializer.timeoutInterval = self.timeout;
NSError *serror = nil;
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
URLString:urlString
parameters:parameters
error:&serror];
if (serror || !req) {
if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}]);
return nil;
}
[self applyHeaders:headers toMutableRequest:req contentType:nil];
#if DEBUG
// 打印请求信息GET
NSString *paramStr = [self kb_prettyJSONStringFromObject:parameters] ?: @"(null)";
KBLOG(@"HTTP GET\nURL: %@\nHeaders: %@\n参数: %@",
req.URL.absoluteString,
req.allHTTPHeaderFields ?: @{},
paramStr);
#endif
return [self startJSONTaskWithRequest:req
autoShowBusinessError:autoShowBusinessError
completion:completion];
}
// 默认 GET自动弹业务错误
- (NSURLSessionDataTask *)GET:(NSString *)path
parameters:(NSDictionary *)parameters
headers:(NSDictionary<NSString *,NSString *> *)headers
completion:(KBNetworkCompletion)completion {
return [self GET:path
parameters:parameters
headers:headers
autoShowBusinessError:YES
completion:completion];
}
// JSON POST可控制业务错误是否由内部弹出
- (NSURLSessionDataTask *)POST:(NSString *)path
jsonBody:(id)jsonBody
headers:(NSDictionary<NSString *,NSString *> *)headers
autoShowBusinessError:(BOOL)autoShowBusinessError
completion:(KBNetworkCompletion)completion {
NSLog(@"[KBNetworkManager] POST called, enabled=%d, path=%@", self.isEnabled, path);
if (![self ensureEnabled:completion]) return nil;
NSString *urlString = [self buildURLStringWithPath:path];
if (!urlString) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; }
// 用 JSON 序列化器生成 JSON Body 的请求
AFJSONRequestSerializer *serializer = [AFJSONRequestSerializer serializer];
serializer.timeoutInterval = self.timeout;
NSError *error = nil;
NSMutableURLRequest *req = [serializer requestWithMethod:@"POST"
URLString:urlString
parameters:jsonBody
error:&error];
if (error) { if (completion) completion(nil, nil, error); return nil; }
[self applyHeaders:headers toMutableRequest:req contentType:nil];
#if DEBUG
// 打印请求信息POST JSON
NSString *bodyStr = [self kb_prettyJSONStringFromObject:jsonBody] ?: @"(null)";
KBLOG(@"HTTP POST\nURL: %@\nHeaders: %@\nJSON: %@",
req.URL.absoluteString,
req.allHTTPHeaderFields ?: @{},
bodyStr);
#endif
return [self startJSONTaskWithRequest:req
autoShowBusinessError:autoShowBusinessError
completion:completion];
}
// 默认 POST自动弹业务错误
- (NSURLSessionDataTask *)POST:(NSString *)path
jsonBody:(id)jsonBody
headers:(NSDictionary<NSString *,NSString *> *)headers
completion:(KBNetworkCompletion)completion {
return [self POST:path
jsonBody:jsonBody
headers:headers
autoShowBusinessError:YES
completion:completion];
}
// 原始二进制 GET用于下载 zip、图片等
- (NSURLSessionDataTask *)GETData:(NSString *)path
parameters:(NSDictionary *)parameters
headers:(NSDictionary<NSString *,NSString *> *)headers
completion:(KBNetworkDataCompletion)completion {
if (!self.isEnabled) {
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorDisabled
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Network disabled (Full Access may be off)")}];
if (completion) completion(nil, nil, e);
return nil;
}
NSString *urlString = [self buildURLStringWithPath:path];
if (!urlString) {
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidURL
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}];
if (completion) completion(nil, nil, e);
return nil;
}
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
serializer.timeoutInterval = self.timeout;
NSError *serror = nil;
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
URLString:urlString
parameters:parameters
error:&serror];
if (serror || !req) {
if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}]);
return nil;
}
[self applyHeaders:headers toMutableRequest:req contentType:nil];
return [self startDataTaskWithRequest:req completion:completion];
}
#pragma mark - Upload (multipart/form-data)
- (NSURLSessionDataTask *)uploadFile:(NSString *)path
fileData:(NSData *)fileData
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
headers:(NSDictionary<NSString *,NSString *> *)headers
autoShowBusinessError:(BOOL)autoShowBusinessError
completion:(KBNetworkCompletion)completion
{
NSLog(@"[KBNetworkManager] UPLOAD called, enabled=%d, path=%@", self.isEnabled, path);
if (![self ensureEnabled:completion]) return nil;
NSString *urlString = [self buildURLStringWithPath:path];
if (!urlString) {
[self fail:KBNetworkErrorInvalidURL completion:completion];
return nil;
}
if (!fileData || fileData.length == 0) {
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidResponse
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Empty file data")}];
if (completion) completion(nil, nil, e);
return nil;
}
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
serializer.timeoutInterval = self.timeout;
NSError *error = nil;
NSMutableURLRequest *req =
[serializer multipartFormRequestWithMethod:@"POST"
URLString:urlString
parameters:nil
constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
// Apifox 后端要求:表单字段名为 "file"
NSString *fieldName = @"file";
NSString *fname = fileName ?: @"file";
NSString *type = mimeType ?: @"application/octet-stream";
[formData appendPartWithFileData:fileData
name:fieldName
fileName:fname
mimeType:type];
} error:&error];
if (error || !req) {
if (completion) {
completion(nil, nil,
error ?: [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidURL
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid upload request")}]);
}
return nil;
}
// 不强行指定 Content-Type留给 AFN 里带 boundary 的那一套
[self applyHeaders:headers toMutableRequest:req contentType:nil];
#if DEBUG
KBLOG(@"HTTP UPLOAD (multipart)\nURL: %@\nHeaders: %@\nfileName: %@\nlength: %lu",
req.URL.absoluteString,
req.allHTTPHeaderFields ?: @{},
fileName,
(unsigned long)fileData.length);
#endif
// 按 JSON 响应处理(和 POST 一样:解析 {code,message,data,...}
return [self startJSONTaskWithRequest:req
autoShowBusinessError:autoShowBusinessError
completion:completion];
}
- (NSURLSessionDataTask *)uploadFile:(NSString *)path
fileData:(NSData *)fileData
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
headers:(NSDictionary<NSString *,NSString *> *)headers
completion:(KBNetworkCompletion)completion
{
return [self uploadFile:path
fileData:fileData
fileName:fileName
mimeType:mimeType
headers:headers
autoShowBusinessError:YES
completion:completion];
}
#pragma mark - Core
- (BOOL)ensureEnabled:(KBNetworkCompletion)completion {
if (!self.isEnabled) {
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDisabled userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Network disabled (Full Access may be off)")}];
if (completion) completion(nil, nil, e);
return NO;
}
return YES;
}
- (NSString *)buildURLStringWithPath:(NSString *)path {
if (path.length == 0) return nil;
if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) {
return path;
}
if (self.baseURL) {
// 1) 统一为目录型 base确保以 / 结尾),否则相对路径会把最后一段当文件替换
NSString *base = self.baseURL.absoluteString ?: @"";
if (![base hasSuffix:@"/"]) { base = [base stringByAppendingString:@"/"]; }
NSURL *dirBase = [NSURL URLWithString:base];
// 2) 防呆:调用方若传了以“/”开头的 path会导致相对路径从根覆盖丢失 /api
NSString *relative = ([path hasPrefix:@"/"]) ? [path substringFromIndex:1] : path;
return [NSURL URLWithString:relative relativeToURL:dirBase].absoluteURL.absoluteString;
}
return path; // 当无 baseURL 且 path 不是完整 URL 时,让 AFN 处理(可能失败)
}
- (void)applyHeaders:(NSDictionary<NSString *,NSString *> *)headers toMutableRequest:(NSMutableURLRequest *)req contentType:(NSString *)contentType {
// 合并默认头与局部头,并注入授权头(若可用)。局部覆盖优先。
NSMutableDictionary *all = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary new];
NSDictionary *auth = [[KBAuthManager shared] authorizationHeader];
[auth enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
if (contentType) all[@"Content-Type"] = contentType;
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
[all enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { [req setValue:obj forHTTPHeaderField:key]; }];
}
- (NSURLSessionDataTask *)startJSONTaskWithRequest:(NSURLRequest *)req
autoShowBusinessError:(BOOL)autoShowBusinessError
completion:(KBNetworkCompletion)completion {
NSLog(@"[KBNetworkManager] startAFTaskWithRequest: %@", req.URL.absoluteString);
// 响应先用原始数据返回,再按 Content-Type 解析 JSON与原实现一致
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
NSLog(@"[KBNetworkManager] task finished, error = %@", error);
// AFN 默认对非 2xx 的状态码返回 error这里先日志再直接回调上层
if (error) {
#if DEBUG
NSInteger status = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0;
KBLOG(@"请求失败\nURL: %@\n状态: %ld\n错误: %@\nUserInfo: %@",
req.URL.absoluteString,
(long)status,
error.localizedDescription,
error.userInfo ?: @{});
#endif
if (completion) completion(nil, response, error);
return;
}
NSData *data = (NSData *)responseObject;
if (![data isKindOfClass:[NSData class]]) {
#if DEBUG
KBLOG(@"无效响应\nURL: %@\n说明: %@", req.URL.absoluteString, KBLocalized(@"未获取到数据"));
#endif
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"No data")}]);
return;
}
NSString *ct = nil;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"];
}
// 更宽松的 JSON 判定Content-Type 里包含 json或首字符是 { / [
BOOL looksJSON = (ct && [[ct lowercaseString] containsString:@"json"]);
if (!looksJSON) {
// 内容嗅探(不依赖服务端声明)
const unsigned char *bytes = data.bytes;
NSUInteger len = data.length;
for (NSUInteger i = 0; !looksJSON && i < len; i++) {
unsigned char c = bytes[i];
if (c == ' ' || c == '\n' || c == '\r' || c == '\t') continue;
looksJSON = (c == '{' || c == '[');
break;
}
}
if (looksJSON) {
NSError *jsonErr = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr];
if (jsonErr) {
#if DEBUG
KBLOG(@"响应解析失败(JSON)\nURL: %@\n错误: %@",
req.URL.absoluteString,
jsonErr.localizedDescription);
#endif
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Failed to parse JSON")}]);
return;
}
// 必须为字典结构,否则视为无效响应
if (![json isKindOfClass:[NSDictionary class]]) {
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Invalid response")}]);
return;
}
#if DEBUG
NSString *pretty = [self kb_prettyJSONStringFromObject:json] ?: @"(null)";
pretty = [self kb_trimmedString:pretty maxLength:4096];
NSInteger status = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0;
KBLOG(@"\n[KBNetwork] 响应成功(JSON)\nURL: %@\n状态: %ld\nContent-Type: %@\n数据: %@\n",
req.URL.absoluteString,
(long)status,
ct ?: @"",
pretty);
#endif
NSDictionary *dict = (NSDictionary *)json;
// 统一解析业务 code约定后端顶层包含 { code, message, data }
NSInteger bizCode = KBBizCodeFromJSONObject(dict);
if (bizCode != NSNotFound && bizCode != KBBizCodeSuccess) {
// 非成功业务 code执行通用处理如 token 失效)并通过 error 方式回调
BOOL handledByAuth = [self kb_handleBizCode:bizCode json:dict response:response];
NSString *msg = KBBizMessageFromJSONObject(dict) ?: KBLocalized(@"Server error");
if (autoShowBusinessError) {
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD dismiss];
[KBHUD showInfo:msg];
});
}
NSError *bizErr = [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorBusiness
userInfo:@{
NSLocalizedDescriptionKey : msg,
@"code" : @(bizCode)
}];
if (completion) completion(dict, response, bizErr);
return;
}
// code 缺失或为成功,按正常成功回调
[KBHUD dismiss];
if (completion) completion(dict, response, nil);
} else {
// 预期 JSON但未检测到 JSON 内容
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Invalid response")}]);
}
}];
[task resume];
return task;
}
// 原始二进制任务
- (NSURLSessionDataTask *)startDataTaskWithRequest:(NSURLRequest *)req
completion:(KBNetworkDataCompletion)completion {
NSLog(@"[KBNetworkManager] startDataTaskWithRequest: %@", req.URL.absoluteString);
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
NSLog(@"[KBNetworkManager] data task finished, error = %@", error);
if (error) {
if (completion) completion(nil, response, error);
return;
}
NSData *data = (NSData *)responseObject;
if (![data isKindOfClass:[NSData class]]) {
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"No data")}]);
return;
}
if (completion) completion(data, response, nil);
}];
[task resume];
return task;
}
#pragma mark - AFHTTPSessionManager
- (AFHTTPSessionManager *)manager {
if (!_manager) {
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration ephemeralSessionConfiguration];
cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
// 不在会话级别设置超时,避免与 per-request 的 serializer.timeoutInterval 产生不一致
if (@available(iOS 11.0, *)) { cfg.waitsForConnectivity = YES; }
_manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:cfg];
// 默认不使用 JSON 解析器,保持原生数据,再按需解析
_manager.responseSerializer = [AFHTTPResponseSerializer serializer];
}
return _manager;
}
#pragma mark - Private helpers
/// 处理通用业务 codetoken 失效、被踢下线等);
/// 返回 YES 表示该 code 已在此处消费(例如已清理登录态并提示),外部无需再提示。
- (BOOL)kb_handleBizCode:(NSInteger)bizCode
json:(id)json
response:(NSURLResponse *)response {
switch (bizCode) {
// 登录态相关:未登录 / 无权限 / 各种 token 异常、被顶号/踢下线等
case KBBizCodeNotLogin:
case KBBizCodeNoAuth:
case KBBizCodeTokenNotFound:
case KBBizCodeTokenInvalid:
case KBBizCodeTokenTimeout:
case KBBizCodeTokenBeReplaced:
case KBBizCodeTokenKickOut:
case KBBizCodeTokenFreeze:
case KBBizCodeTokenNoPrefix:
case KBBizCodeForbidden: {
// 登录态异常:统一清理本地会话并提示用户重新登录
NSString *msg = KBBizMessageFromJSONObject(json);
if (msg.length == 0) {
msg = KBLocalized(@"Your session has expired. Please sign in again.");
}
dispatch_async(dispatch_get_main_queue(), ^{
// 清理本地用户会话Keychain + 用户缓存)
[[KBUserSessionManager shared] logout];
// 友好提示
[KBHUD showInfo:msg];
// 广播通知,便于 UI 侧跳转登录页等
NSDictionary *info = @{ @"code": @(bizCode),
@"message": msg ?: @"" };
[[NSNotificationCenter defaultCenter] postNotificationName:@"KBUserSessionInvalidNotification"
object:nil
userInfo:info];
});
return YES;
} break;
default:
break;
}
return NO;
}
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion {
NSString *msg = KBLocalized(@"Network error");
switch (code) {
case KBNetworkErrorDisabled: msg = KBLocalized(@"Network disabled (Full Access may be off)"); break;
case KBNetworkErrorInvalidURL: msg = KBLocalized(@"Invalid URL"); break;
case KBNetworkErrorInvalidResponse: msg = KBLocalized(@"Invalid response"); break;
case KBNetworkErrorDecodeFailed: msg = KBLocalized(@"Parse failed"); break;
case KBNetworkErrorBusiness: msg = KBLocalized(@"Server error"); break;
default: break;
}
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
code:code
userInfo:@{NSLocalizedDescriptionKey: msg}];
if (completion) completion(nil, nil, e);
}
@end
#pragma mark - Debug helpers
#if DEBUG
@implementation KBNetworkManager (Debug)
// 将对象转为漂亮的 JSON 字符串;否则返回 description
- (NSString *)kb_prettyJSONStringFromObject:(id)obj {
if (!obj || obj == (id)kCFNull) return nil;
if (![NSJSONSerialization isValidJSONObject:obj]) {
// 非标准 JSON 对象,直接 description
return [obj description];
}
NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:NSJSONWritingPrettyPrinted error:NULL];
if (!data) return [obj description];
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ?: [obj description];
}
// 尝试把二进制数据转为可读文本;失败则返回占位带长度
- (NSString *)kb_textFromData:(NSData *)data {
if (!data) return @"(null)";
if (data.length == 0) return @"";
// 优先 UTF-8
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (text.length > 0) return text;
// 再尝试常见编码
NSArray *encs = @[@(NSASCIIStringEncoding), @(NSISOLatin1StringEncoding), @(NSUnicodeStringEncoding)];
for (NSNumber *n in encs) {
text = [[NSString alloc] initWithData:data encoding:n.unsignedIntegerValue];
if (text.length > 0) return text;
}
return [NSString stringWithFormat:@"<binary %lu bytes>", (unsigned long)data.length];
}
// 过长时裁剪,避免刷屏
- (NSString *)kb_trimmedString:(NSString *)s maxLength:(NSUInteger)maxLen {
if (!s) return @"";
if (s.length <= maxLen) return s;
NSString *head = [s substringToIndex:maxLen];
return [head stringByAppendingString:@"\n...<trimmed>..."];
}
@end
#endif