// // KBNetworkManager.m // CustomKeyboard // #import "KBNetworkManager.h" #import "AFNetworking.h" #import "KBAuthManager.h" //#import "KBUserSessionManager.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 @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 : @""; _defaultHeaders = @{ @"Accept": @"*/*", @"Accept-Language": lang }; // 设置基础域名,路径可相对该地址拼接 _baseURL = [NSURL URLWithString:KB_BASE_URL]; } return self; } - (void)getSignWithParare:(NSDictionary *)bodyParams{ NSString *appId = @"loveKeyboard"; NSString *secret = @"kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H"; // 和服务端保持一致 NSString *timestamp = [KBSignUtils currentTimestamp]; NSString *nonce = [KBSignUtils generateNonceWithLength:16]; // 1. 组装参与签名的所有参数 NSMutableDictionary *signParams = [NSMutableDictionary dictionary]; signParams[@"appId"] = appId; signParams[@"timestamp"] = timestamp; signParams[@"nonce"] = nonce; // 把 body 里的字段也加入签名参数 [bodyParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { if ([obj isKindOfClass:[NSString class]]) { signParams[key] = obj; } else { signParams[key] = [obj description]; } }]; NSString *sign = [KBSignUtils signWithParams:signParams secret:secret]; // 将签名相关字段合并进默认请求头 NSMutableDictionary *headers = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary dictionary]; if (sign.length > 0) { headers[@"X-Sign"] = sign; } headers[@"X-App-Id"] = appId; headers[@"X-Timestamp"] = timestamp; headers[@"X-Nonce"] = nonce; // 触发 copy 语义,确保对外仍是不可变字典 self.defaultHeaders = headers; } #pragma mark - Public - (NSURLSessionDataTask *)GET:(NSString *)path parameters:(NSDictionary *)parameters headers:(NSDictionary *)headers completion:(KBNetworkCompletion)completion { [self getSignWithParare:parameters]; 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]; return [self startAFJSONTaskWithRequest:req completion:completion]; } - (NSURLSessionDataTask *)POST:(NSString *)path jsonBody:(id)jsonBody headers:(NSDictionary *)headers completion:(KBNetworkCompletion)completion { [self getSignWithParare:jsonBody]; 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 startAFJSONTaskWithRequest:req completion:completion]; } - (NSURLSessionDataTask *)GETData:(NSString *)path parameters:(NSDictionary *)parameters headers:(NSDictionary *)headers completion:(KBNetworkDataCompletion)completion { [self getSignWithParare:parameters]; 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 startAFDataTaskWithRequest:req 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) { // 统一为目录型 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]; NSString *token = [KBAuthManager shared].current.accessToken; if (token.length > 0) { all[@"auth-token"] = token; } else { [all removeObjectForKey:@"auth-token"]; } 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 *)startAFJSONTaskWithRequest:(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: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 (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 (completion) completion((NSDictionary *)json, response, nil); } else { if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Invalid response")}]); } }]; [task resume]; return task; } - (NSURLSessionDataTask *)startAFDataTaskWithRequest:(NSURLRequest *)req completion:(KBNetworkDataCompletion)completion { self.manager.responseSerializer = [AFHTTPResponseSerializer serializer]; NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *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 - (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; default: break; } NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: msg}]; if (completion) completion(nil, nil, e); } @end