// // KBNetworkManager.m // CustomKeyboard // #import "KBNetworkManager.h" #import "AFNetworking.h" #import "KBAuthManager.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 @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 拒绝 _defaultHeaders = @{ @"Accept": @"*/*" }; // 设置基础域名,路径可相对该地址拼接 _baseURL = [NSURL URLWithString:KB_BASE_URL]; } return self; } #pragma mark - Public - (NSURLSessionDataTask *)GET:(NSString *)path parameters:(NSDictionary *)parameters headers:(NSDictionary *)headers completion:(KBNetworkCompletion)completion { 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: @"无效的URL"}]); return nil; } [self applyHeaders:headers toMutableRequest:req contentType:nil]; return [self startAFTaskWithRequest:req completion:completion]; } - (NSURLSessionDataTask *)POST:(NSString *)path jsonBody:(id)jsonBody headers:(NSDictionary *)headers completion:(KBNetworkCompletion)completion { 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]; return [self startAFTaskWithRequest:req completion:completion]; } #pragma mark - Core - (BOOL)ensureEnabled:(KBNetworkCompletion)completion { if (!self.isEnabled) { NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDisabled userInfo:@{NSLocalizedDescriptionKey: @"网络未启用(可能未开启完全访问)"}]; 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) { // 统一为目录型 base(以 / 结尾),并剥掉 path 的前导 /,避免覆盖 base 路径 NSString *base = self.baseURL.absoluteString ?: @""; if (![base hasSuffix:@"/"]) { base = [base stringByAppendingString:@"/"]; } NSURL *dirBase = [NSURL URLWithString:base]; 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 *)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 *)startAFTaskWithRequest:(NSURLRequest *)req completion:(KBNetworkCompletion)completion { // 响应先用原始数据返回,再按 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) { // AFN 默认对非 2xx 的状态码返回 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:@"无数据"}]); 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 (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:@"JSON解析失败"}]); return; } if (completion) completion(json, response, nil); } else { 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 - (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion { NSString *msg = @"网络错误"; switch (code) { case KBNetworkErrorDisabled: msg = @"网络未启用(可能未开启完全访问)"; break; case KBNetworkErrorInvalidURL: msg = @"无效的URL"; break; case KBNetworkErrorInvalidResponse: msg = @"无效的响应"; break; case KBNetworkErrorDecodeFailed: msg = @"解析失败"; break; default: break; } NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: msg}]; if (completion) completion(nil, nil, e); } @end