2025-10-29 20:57:45 +08:00
//
// KBNetworkManager . m
// CustomKeyboard
//
# import "KBNetworkManager.h"
2025-12-02 21:32:49 +08:00
# import < TargetConditionals . h >
2025-10-29 20:57:45 +08:00
# import "AFNetworking.h"
2025-10-31 16:06:54 +08:00
# import "KBAuthManager.h"
2025-12-02 21:32:49 +08:00
# import "KBBizCode.h"
// 仅 在 主 App 内 需 要 的 会 话 管 理 与 HUD 组 件
# import "KBUserSessionManager.h"
# import "KBHUD.h"
2025-12-04 20:27:26 +08:00
# import "KBSignUtils.h"
2025-10-29 20:57:45 +08:00
NSErrorDomain const KBNetworkErrorDomain = @ "com.company.keyboard.network" ;
@ interface KBNetworkManager ( )
@ property ( nonatomic , strong ) AFHTTPSessionManager * manager ; // AFN 管 理 器 ( ephemeral 配 置 )
// 私 有 错 误 派 发
- ( void ) fail : ( KBNetworkError ) code completion : ( KBNetworkCompletion ) completion ;
@ end
2025-11-13 15:34:56 +08:00
# 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
2025-10-29 20:57:45 +08:00
@ 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 ;
2025-12-04 20:34:23 +08:00
2025-12-02 20:33:17 +08:00
NSString * lang = [ KBLocalizationManager shared ] . currentLanguageCode ? : KBLanguageCodeEnglish ;
2025-12-03 20:31:33 +08:00
NSString * token = [ KBUserSessionManager shared ] . accessToken ? [ KBUserSessionManager shared ] . accessToken : @ "" ;
2025-12-04 20:27:26 +08:00
// 如 果 还 有 query 参 数 也 塞 进 来
// signParams [ @ "lang" ] = @ "zh" ;
2025-12-04 20:34:23 +08:00
2025-12-04 20:27:26 +08:00
// 2. 计 算 签 名
2025-12-02 19:39:37 +08:00
_defaultHeaders = @ {
@ "Accept" : @ "*/*" ,
2025-12-03 20:14:14 +08:00
@ "Accept-Language" : lang ,
2025-12-04 20:27:26 +08:00
@ "auth-token" : token ,
2025-12-02 19:39:37 +08:00
} ;
2025-10-30 13:10:33 +08:00
// 设 置 基 础 域 名 , 路 径 可 相 对 该 地 址 拼 接
_baseURL = [ NSURL URLWithString : KB_BASE _URL ] ;
2025-10-29 20:57:45 +08:00
}
return self ;
}
2025-12-04 20:34:23 +08:00
- ( void ) getSignWithParare : ( NSDictionary * ) bodyParams {
2025-12-04 20:57:39 +08:00
2025-12-04 20:34:23 +08:00
NSString * appId = @ "loveKeyboard" ;
NSString * secret = @ "kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H" ; // 和 服 务 端 保 持 一 致
NSString * timestamp = [ KBSignUtils currentTimestamp ] ;
NSString * nonce = [ KBSignUtils generateNonceWithLength : 16 ] ;
// 1. 组 装 参 与 签 名 的 所 有 参 数
NSMutableDictionary < NSString * , NSString * > * 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 ] ;
2025-12-04 20:57:39 +08:00
// 将 签 名 相 关 字 段 合 并 进 默 认 请 求 头
NSMutableDictionary < NSString * , NSString * > * 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 ;
2025-12-04 20:34:23 +08:00
}
2025-12-03 14:30:02 +08:00
#
2025-10-29 20:57:45 +08:00
# pragma mark - Public
2025-12-03 14:30:02 +08:00
// JSON GET : 可 控 制 业 务 错 误 是 否 由 内 部 弹 出
2025-10-29 20:57:45 +08:00
- ( NSURLSessionDataTask * ) GET : ( NSString * ) path
2025-12-03 14:30:02 +08:00
parameters : ( NSDictionary * ) parameters
headers : ( NSDictionary < NSString * , NSString * > * ) headers
autoShowBusinessError : ( BOOL ) autoShowBusinessError
completion : ( KBNetworkCompletion ) completion {
2025-11-25 21:50:07 +08:00
NSLog ( @ "[KBNetworkManager] GET called, enabled=%d, path=%@" , self . isEnabled , path ) ;
2025-12-04 20:57:39 +08:00
[ self getSignWithParare : parameters ] ;
2025-10-29 20:57:45 +08:00
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 ;
2025-11-13 15:34:56 +08:00
NSError * serror = nil ;
2025-10-29 20:57:45 +08:00
NSMutableURLRequest * req = [ serializer requestWithMethod : @ "GET"
URLString : urlString
parameters : parameters
2025-11-13 15:34:56 +08:00
error : & serror ] ;
if ( serror || ! req ) {
2025-11-17 20:07:39 +08:00
if ( completion ) completion ( nil , nil , serror ? : [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorInvalidURL userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Invalid URL" ) } ] ) ;
2025-11-13 15:34:56 +08:00
return nil ;
}
2025-10-29 20:57:45 +08:00
[ self applyHeaders : headers toMutableRequest : req contentType : nil ] ;
2025-11-13 15:34:56 +08:00
# if DEBUG
// 打 印 请 求 信 息 ( GET )
NSString * paramStr = [ self kb_prettyJSONStringFromObject : parameters ] ? : @ "(null)" ;
KBLOG ( @ "HTTP GET\nURL: %@\nHeaders: %@\n参数: %@" ,
req . URL . absoluteString ,
req . allHTTPHeaderFields ? : @ { } ,
paramStr ) ;
# endif
2025-12-03 14:30:02 +08:00
return [ self startJSONTaskWithRequest : req
autoShowBusinessError : autoShowBusinessError
completion : completion ] ;
2025-10-29 20:57:45 +08:00
}
2025-12-03 14:30:02 +08:00
// 默 认 GET : 自 动 弹 业 务 错 误
- ( NSURLSessionDataTask * ) GET : ( NSString * ) path
parameters : ( NSDictionary * ) parameters
headers : ( NSDictionary < NSString * , NSString * > * ) headers
completion : ( KBNetworkCompletion ) completion {
2025-12-04 20:57:39 +08:00
[ self getSignWithParare : parameters ] ;
2025-12-03 14:30:02 +08:00
return [ self GET : path
parameters : parameters
headers : headers
autoShowBusinessError : YES
completion : completion ] ;
}
// JSON POST : 可 控 制 业 务 错 误 是 否 由 内 部 弹 出
2025-10-29 20:57:45 +08:00
- ( NSURLSessionDataTask * ) POST : ( NSString * ) path
2025-12-03 14:30:02 +08:00
jsonBody : ( id ) jsonBody
headers : ( NSDictionary < NSString * , NSString * > * ) headers
autoShowBusinessError : ( BOOL ) autoShowBusinessError
completion : ( KBNetworkCompletion ) completion {
NSLog ( @ "[KBNetworkManager] POST called, enabled=%d, path=%@" , self . isEnabled , path ) ;
2025-12-04 20:57:39 +08:00
[ self getSignWithParare : jsonBody ] ;
2025-10-29 20:57:45 +08:00
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 ] ;
2025-11-13 15:34:56 +08:00
# if DEBUG
// 打 印 请 求 信 息 ( POST JSON )
NSString * bodyStr = [ self kb_prettyJSONStringFromObject : jsonBody ] ? : @ "(null)" ;
KBLOG ( @ "HTTP POST\nURL: %@\nHeaders: %@\nJSON: %@" ,
req . URL . absoluteString ,
req . allHTTPHeaderFields ? : @ { } ,
bodyStr ) ;
# endif
2025-12-03 14:30:02 +08:00
return [ self startJSONTaskWithRequest : req
autoShowBusinessError : autoShowBusinessError
completion : completion ] ;
}
// 默 认 POST : 自 动 弹 业 务 错 误
- ( NSURLSessionDataTask * ) POST : ( NSString * ) path
jsonBody : ( id ) jsonBody
headers : ( NSDictionary < NSString * , NSString * > * ) headers
completion : ( KBNetworkCompletion ) completion {
2025-12-04 20:57:39 +08:00
[ self getSignWithParare : jsonBody ] ;
2025-12-03 14:30:02 +08:00
return [ self POST : path
jsonBody : jsonBody
headers : headers
autoShowBusinessError : YES
completion : completion ] ;
2025-12-03 13:54:57 +08:00
}
// 原 始 二 进 制 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 ] ;
2025-10-29 20:57:45 +08:00
}
2025-12-04 13:37:11 +08:00
# 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 ] ;
}
2025-10-29 20:57:45 +08:00
# pragma mark - Core
- ( BOOL ) ensureEnabled : ( KBNetworkCompletion ) completion {
if ( ! self . isEnabled ) {
2025-11-17 20:07:39 +08:00
NSError * e = [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorDisabled userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Network disabled (Full Access may be off)" ) } ] ;
2025-10-29 20:57:45 +08:00
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 ) {
2025-11-13 16:23:46 +08:00
// 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 ;
2025-10-29 20:57:45 +08:00
}
return path ; // 当 无 baseURL 且 path 不 是 完 整 URL 时 , 让 AFN 处 理 ( 可 能 失 败 )
}
- ( void ) applyHeaders : ( NSDictionary < NSString * , NSString * > * ) headers toMutableRequest : ( NSMutableURLRequest * ) req contentType : ( NSString * ) contentType {
2025-10-31 16:06:54 +08:00
// 合 并 默 认 头 与 局 部 头 , 并 注 入 授 权 头 ( 若 可 用 ) 。 局 部 覆 盖 优 先 。
2025-10-29 20:57:45 +08:00
NSMutableDictionary * all = [ self . defaultHeaders mutableCopy ] ? : [ NSMutableDictionary new ] ;
2025-10-31 16:06:54 +08:00
NSDictionary * auth = [ [ KBAuthManager shared ] authorizationHeader ] ;
[ auth enumerateKeysAndObjectsUsingBlock : ^ ( NSString * key , NSString * obj , BOOL * stop ) { all [ key ] = obj ; } ] ;
2025-10-29 20:57:45 +08:00
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 ] ; } ] ;
}
2025-12-03 13:54:57 +08:00
- ( NSURLSessionDataTask * ) startJSONTaskWithRequest : ( NSURLRequest * ) req
2025-12-03 14:30:02 +08:00
autoShowBusinessError : ( BOOL ) autoShowBusinessError
2025-12-03 13:54:57 +08:00
completion : ( KBNetworkCompletion ) completion {
2025-11-25 21:50:07 +08:00
NSLog ( @ "[KBNetworkManager] startAFTaskWithRequest: %@" , req . URL . absoluteString ) ;
2025-10-29 20:57:45 +08:00
// 响 应 先 用 原 始 数 据 返 回 , 再 按 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 ) {
2025-11-25 21:50:07 +08:00
NSLog ( @ "[KBNetworkManager] task finished, error = %@" , error ) ;
2025-11-13 15:34:56 +08:00
// AFN 默 认 对 非 2 xx 的 状 态 码 返 回 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 ;
}
2025-10-29 20:57:45 +08:00
NSData * data = ( NSData * ) responseObject ;
if ( ! [ data isKindOfClass : [ NSData class ] ] ) {
2025-11-17 20:07:39 +08:00
# if DEBUG
KBLOG ( @ "无效响应\nURL: %@\n说明: %@" , req . URL . absoluteString , KBLocalized ( @ "未获取到数据" ) ) ;
# endif
2025-12-03 13:54:57 +08:00
if ( completion ) completion ( nil , response , [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorInvalidResponse userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "No data" ) } ] ) ;
return ;
}
2025-10-29 20:57:45 +08:00
NSString * ct = nil ;
if ( [ response isKindOfClass : [ NSHTTPURLResponse class ] ] ) {
ct = ( ( NSHTTPURLResponse * ) response ) . allHeaderFields [ @ "Content-Type" ] ;
}
2025-11-13 15:34:56 +08:00
// 更 宽 松 的 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 ;
}
}
2025-10-29 20:57:45 +08:00
if ( looksJSON ) {
NSError * jsonErr = nil ;
id json = [ NSJSONSerialization JSONObjectWithData : data options : 0 error : & jsonErr ] ;
2025-11-13 15:34:56 +08:00
if ( jsonErr ) {
2025-11-17 20:07:39 +08:00
# if DEBUG
2025-11-13 15:34:56 +08:00
KBLOG ( @ "响应解析失败(JSON)\nURL: %@\n错误: %@" ,
req . URL . absoluteString ,
jsonErr . localizedDescription ) ;
2025-11-17 20:07:39 +08:00
# endif
if ( completion ) completion ( nil , response , [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorDecodeFailed userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Failed to parse JSON" ) } ] ) ;
2025-11-13 15:34:56 +08:00
return ;
}
2025-12-03 13:54:57 +08:00
// 必 须 为 字 典 结 构 , 否 则 视 为 无 效 响 应
if ( ! [ json isKindOfClass : [ NSDictionary class ] ] ) {
if ( completion ) completion ( nil , response , [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorInvalidResponse userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Invalid response" ) } ] ) ;
return ;
}
2025-11-13 15:34:56 +08:00
# 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 ;
2025-12-02 21:32:49 +08:00
KBLOG ( @ "\n[KBNetwork] 响应成功(JSON)\nURL: %@\n状态: %ld\nContent-Type: %@\n数据: %@\n" ,
2025-11-13 15:34:56 +08:00
req . URL . absoluteString ,
( long ) status ,
ct ? : @ "" ,
pretty ) ;
# endif
2025-12-03 13:54:57 +08:00
NSDictionary * dict = ( NSDictionary * ) json ;
2025-12-02 21:32:49 +08:00
// 统 一 解 析 业 务 code : 约 定 后 端 顶 层 包 含 { code , message , data }
2025-12-03 13:54:57 +08:00
NSInteger bizCode = KBBizCodeFromJSONObject ( dict ) ;
if ( bizCode ! = NSNotFound && bizCode ! = KBBizCodeSuccess ) {
2025-12-03 14:30:02 +08:00
// 非 成 功 业 务 code : 执 行 通 用 处 理 ( 如 token 失 效 ) 并 通 过 error 方 式 回 调
BOOL handledByAuth = [ self kb_handleBizCode : bizCode json : dict response : response ] ;
NSString * msg = KBBizMessageFromJSONObject ( dict ) ? : KBLocalized ( @ "Server error" ) ;
2025-12-12 14:16:48 +08:00
if ( handledByAuth = = false ) {
if ( autoShowBusinessError ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ KBHUD showInfo : msg ] ;
} ) ;
}
2025-12-02 21:32:49 +08:00
}
2025-12-03 14:30:02 +08:00
NSError * bizErr = [ NSError errorWithDomain : KBNetworkErrorDomain
code : KBNetworkErrorBusiness
userInfo : @ {
NSLocalizedDescriptionKey : msg ,
@ "code" : @ ( bizCode )
} ] ;
2025-12-12 16:09:14 +08:00
if ( completion ) completion ( dict , response , bizErr ) ;
2025-12-03 14:30:02 +08:00
return ;
}
2025-12-02 21:32:49 +08:00
// code 缺 失 或 为 成 功 , 按 正 常 成 功 回 调
2025-12-03 13:54:57 +08:00
if ( completion ) completion ( dict , response , nil ) ;
2025-10-29 20:57:45 +08:00
} else {
2025-12-03 13:54:57 +08:00
// 预 期 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 ;
2025-10-29 20:57:45 +08:00
}
2025-12-03 13:54:57 +08:00
if ( completion ) completion ( data , response , nil ) ;
2025-10-29 20:57:45 +08:00
} ] ;
[ task resume ] ;
return task ;
}
# pragma mark - AFHTTPSessionManager
- ( AFHTTPSessionManager * ) manager {
if ( ! _manager ) {
NSURLSessionConfiguration * cfg = [ NSURLSessionConfiguration ephemeralSessionConfiguration ] ;
cfg . requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData ;
2025-11-13 15:34:56 +08:00
// 不 在 会 话 级 别 设 置 超 时 , 避 免 与 per - request 的 serializer . timeoutInterval 产 生 不 一 致
2025-10-29 20:57:45 +08:00
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
2025-12-03 14:30:02 +08:00
// / 处 理 通 用 业 务 code ( token 失 效 、 被 踢 下 线 等 ) ;
// / 返 回 YES 表 示 该 code 已 在 此 处 消 费 ( 例 如 已 清 理 登 录 态 并 提 示 ) , 外 部 无 需 再 提 示 。
- ( BOOL ) kb_handleBizCode : ( NSInteger ) bizCode
2025-12-02 21:32:49 +08:00
json : ( id ) json
response : ( NSURLResponse * ) response {
switch ( bizCode ) {
2025-12-03 12:55:51 +08:00
// 登 录 态 相 关 : 未 登 录 / 无 权 限 / 各 种 token 异 常 、 被 顶 号 / 踢 下 线 等
case KBBizCodeNotLogin :
case KBBizCodeNoAuth :
case KBBizCodeTokenNotFound :
2025-12-02 21:32:49 +08:00
case KBBizCodeTokenInvalid :
2025-12-03 12:55:51 +08:00
case KBBizCodeTokenTimeout :
case KBBizCodeTokenBeReplaced :
case KBBizCodeTokenKickOut :
case KBBizCodeTokenFreeze :
case KBBizCodeTokenNoPrefix :
case KBBizCodeForbidden : {
2025-12-02 21:32:49 +08:00
// 登 录 态 异 常 : 统 一 清 理 本 地 会 话 并 提 示 用 户 重 新 登 录
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 ] ;
2025-12-12 14:16:48 +08:00
[ [ KBUserSessionManager shared ] goLoginVC ] ;
2025-12-02 21:32:49 +08:00
// 友 好 提 示
[ KBHUD showInfo : msg ] ;
// 广 播 通 知 , 便 于 UI 侧 跳 转 登 录 页 等
NSDictionary * info = @ { @ "code" : @ ( bizCode ) ,
@ "message" : msg ? : @ "" } ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : @ "KBUserSessionInvalidNotification"
object : nil
userInfo : info ] ;
} ) ;
2025-12-03 14:30:02 +08:00
return YES ;
2025-12-02 21:32:49 +08:00
} break ;
default :
break ;
}
2025-12-03 14:30:02 +08:00
return NO ;
2025-12-02 21:32:49 +08:00
}
2025-11-17 20:07:39 +08:00
- ( 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 ;
2025-12-02 21:32:49 +08:00
case KBNetworkErrorBusiness : msg = KBLocalized ( @ "Server error" ) ; break ;
2025-11-17 20:07:39 +08:00
default : break ;
}
NSError * e = [ NSError errorWithDomain : KBNetworkErrorDomain
code : code
userInfo : @ { NSLocalizedDescriptionKey : msg } ] ;
if ( completion ) completion ( nil , nil , e ) ;
2025-10-29 20:57:45 +08:00
}
@ end
2025-11-13 15:34:56 +08:00
# 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