- 新增健康知识视图(标签切换+搜索+列表)和健康资讯视图(纯列表无搜索) - 对接健康栏目列表和文章分页接口,资讯无子分类直接用父级ID - 接入更多/搜索/详情H5跳转,统一用通用加密串传参 - 键盘弹起时自动避让,从H5回来刷新文章收藏数 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
223 lines
8.9 KiB
Objective-C
223 lines
8.9 KiB
Objective-C
//
|
|
// XJHealthKnowledgeManager.m
|
|
// XinjiangProject
|
|
//
|
|
// Created by Apple on 2026/6/29.
|
|
//
|
|
|
|
#import "XJHealthKnowledgeManager.h"
|
|
#import <CommonCrypto/CommonHMAC.h>
|
|
|
|
#pragma mark - ————— Manager —————
|
|
|
|
@interface XJHealthKnowledgeManager ()
|
|
|
|
@property (nonatomic, copy) NSString * cachedToken;
|
|
|
|
@end
|
|
|
|
@implementation XJHealthKnowledgeManager
|
|
|
|
+ (instancetype)sharedManager {
|
|
static XJHealthKnowledgeManager * instance = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
instance = [[XJHealthKnowledgeManager alloc] init];
|
|
});
|
|
return instance;
|
|
}
|
|
|
|
#pragma mark - ————— HMAC-SHA256 —————
|
|
|
|
- (void)clearToken {
|
|
self.cachedToken = nil;
|
|
DLog(@"[健康知识] Token 已清除");
|
|
}
|
|
|
|
+ (NSString *)hmacSHA256:(NSString *)data key:(NSString *)key {
|
|
const char * cKey = [key cStringUsingEncoding:NSUTF8StringEncoding];
|
|
const char * cData = [data cStringUsingEncoding:NSUTF8StringEncoding];
|
|
unsigned char result[CC_SHA256_DIGEST_LENGTH];
|
|
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), result);
|
|
|
|
NSMutableString * hash = [[NSMutableString alloc] initWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
|
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
|
|
[hash appendFormat:@"%02x", result[i]];
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
#pragma mark - ————— 获取 Token —————
|
|
|
|
- (void)getTokenWithUserId:(NSString *)userId
|
|
workNo:(NSString *)workNo
|
|
success:(void (^)(NSString *))success
|
|
failure:(void (^)(NSString *))failure {
|
|
|
|
NSString * urlString = [NSString stringWithFormat:@"%@%@", kHealthKnowledgeBaseURL, kHealthKnowledgeTokenPath];
|
|
|
|
// 构建请求体
|
|
NSDictionary * bodyDict = @{ @"userId": userId ?: @"",
|
|
@"workNo": workNo ?: @"" };
|
|
NSData * bodyData = [NSJSONSerialization dataWithJSONObject:bodyDict options:0 error:nil];
|
|
NSString * bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
|
|
|
|
// 签名
|
|
NSString * timestamp = [NSString stringWithFormat:@"%.0f", [[NSDate date] timeIntervalSince1970]];
|
|
NSString * stringToSign = [NSString stringWithFormat:@"%@%@", timestamp, bodyString];
|
|
NSString * sign = [[self class] hmacSHA256:stringToSign key:kHealthKnowledgeSecretKey];
|
|
|
|
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
|
|
request.HTTPMethod = @"POST";
|
|
[request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
|
|
[request setValue:timestamp forHTTPHeaderField:@"X-Timestamp"];
|
|
[request setValue:sign forHTTPHeaderField:@"X-Sign"];
|
|
request.HTTPBody = bodyData;
|
|
|
|
DLog(@"[健康知识] 请求 Token URL: %@", urlString);
|
|
DLog(@"[健康知识] 签名串: %@", stringToSign);
|
|
DLog(@"[健康知识] X-Sign: %@", sign);
|
|
|
|
[self sendRequest:request completion:^(NSDictionary * json) {
|
|
DLog(@"[健康知识] Token 响应: %@", json);
|
|
if ([json[@"code"] integerValue] == 200) {
|
|
NSString * token = json[@"data"][@"token"];
|
|
if (!ValidStr(token)) {
|
|
self.cachedToken = token;
|
|
DLog(@"[健康知识] Token 获取成功: %@", token);
|
|
if (success) success(token);
|
|
} else {
|
|
DLog(@"[健康知识] Token 为空");
|
|
if (failure) failure(@"token 为空");
|
|
}
|
|
} else {
|
|
DLog(@"[健康知识] Token 失败: %@", json[@"msg"]);
|
|
if (failure) failure(json[@"msg"] ?: @"获取 token 失败");
|
|
}
|
|
} failure:^(NSString * errorMsg) {
|
|
DLog(@"[健康知识] Token 请求异常: %@", errorMsg);
|
|
if (failure) failure(errorMsg);
|
|
}];
|
|
}
|
|
|
|
#pragma mark - ————— 栏目列表 —————
|
|
|
|
- (void)getCategoryListWithToken:(NSString *)token
|
|
success:(void (^)(NSArray<XJHealthCategoryModel *> *))success
|
|
failure:(void (^)(NSString *))failure {
|
|
|
|
NSString * urlString = [NSString stringWithFormat:@"%@%@", kHealthKnowledgeBaseURL, kHealthKnowledgeCategoryPath];
|
|
|
|
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
|
|
request.HTTPMethod = @"GET";
|
|
[request setValue:token forHTTPHeaderField:@"token"];
|
|
|
|
[self sendRequest:request completion:^(NSDictionary * json) {
|
|
if ([json[@"code"] integerValue] == 200) {
|
|
NSArray * dataArray = json[@"data"];
|
|
NSMutableArray * categories = [[NSMutableArray alloc] init];
|
|
if ([dataArray isKindOfClass:[NSArray class]]) {
|
|
for (NSDictionary * dict in dataArray) {
|
|
XJHealthCategoryModel * m = [XJHealthCategoryModel modelWithDictionary:dict];
|
|
[categories addObject:m];
|
|
}
|
|
}
|
|
if (success) success(categories);
|
|
} else {
|
|
if (failure) failure(json[@"msg"] ?: @"获取栏目列表失败");
|
|
}
|
|
} failure:^(NSString * errorMsg) {
|
|
if (failure) failure(errorMsg);
|
|
}];
|
|
}
|
|
|
|
#pragma mark - ————— 分页查询文章 —————
|
|
|
|
- (void)getArticleListWithToken:(NSString *)token
|
|
categoryId:(NSInteger)categoryId
|
|
articleType:(NSString *)articleType
|
|
pageNum:(NSInteger)pageNum
|
|
pageSize:(NSInteger)pageSize
|
|
success:(void (^)(NSArray<XJHealthArticleModel *> *, NSInteger))success
|
|
failure:(void (^)(NSString *))failure {
|
|
|
|
NSMutableString * urlString = [NSMutableString stringWithFormat:@"%@%@", kHealthKnowledgeBaseURL, kHealthKnowledgeArticleListPath];
|
|
[urlString appendFormat:@"?categoryId=%ld&pageNum=%ld&pageSize=%ld&showAppHomepage=Y",
|
|
(long)categoryId, (long)pageNum, (long)pageSize];
|
|
|
|
if (!ValidStr(articleType)) {
|
|
[urlString appendFormat:@"&articleType=%@", articleType];
|
|
}
|
|
|
|
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
|
|
request.HTTPMethod = @"GET";
|
|
[request setValue:token forHTTPHeaderField:@"token"];
|
|
|
|
[self sendRequest:request completion:^(NSDictionary * json) {
|
|
if ([json[@"code"] integerValue] == 200) {
|
|
NSArray * rows = json[@"rows"];
|
|
NSInteger total = [json[@"total"] integerValue];
|
|
NSMutableArray * articles = [[NSMutableArray alloc] init];
|
|
if ([rows isKindOfClass:[NSArray class]]) {
|
|
for (NSDictionary * dict in rows) {
|
|
XJHealthArticleModel * m = [XJHealthArticleModel modelWithDictionary:dict];
|
|
[articles addObject:m];
|
|
}
|
|
}
|
|
if (success) success(articles, total);
|
|
} else {
|
|
if (failure) failure(json[@"msg"] ?: @"获取文章列表失败");
|
|
}
|
|
} failure:^(NSString * errorMsg) {
|
|
if (failure) failure(errorMsg);
|
|
}];
|
|
}
|
|
|
|
#pragma mark - ————— 通用请求 —————
|
|
|
|
- (void)sendRequest:(NSURLRequest *)request
|
|
completion:(void (^)(NSDictionary * json))completion
|
|
failure:(void (^)(NSString * errorMsg))failure {
|
|
|
|
DLog(@"[健康知识] >>> 请求: %@ %@", request.HTTPMethod, request.URL.absoluteString);
|
|
|
|
NSURLSession * session = [NSURLSession sharedSession];
|
|
NSURLSessionDataTask * task = [session dataTaskWithRequest:request
|
|
completionHandler:^(NSData * _Nullable data,
|
|
NSURLResponse * _Nullable response,
|
|
NSError * _Nullable error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (error) {
|
|
DLog(@"[健康知识] <<< 网络错误: %@", error.localizedDescription);
|
|
if (failure) failure(error.localizedDescription);
|
|
return;
|
|
}
|
|
|
|
NSHTTPURLResponse * httpResp = (NSHTTPURLResponse *)response;
|
|
NSInteger statusCode = httpResp.statusCode;
|
|
DLog(@"[健康知识] <<< HTTP %ld", (long)statusCode);
|
|
|
|
if (statusCode != 200) {
|
|
NSString * msg = [NSString stringWithFormat:@"HTTP %ld", (long)statusCode];
|
|
DLog(@"[健康知识] <<< 请求失败: %@", msg);
|
|
if (failure) failure(msg);
|
|
return;
|
|
}
|
|
|
|
NSError * jsonError = nil;
|
|
NSDictionary * json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
|
|
if (jsonError || ![json isKindOfClass:[NSDictionary class]]) {
|
|
DLog(@"[健康知识] <<< JSON 解析失败");
|
|
if (failure) failure(@"数据解析失败");
|
|
return;
|
|
}
|
|
|
|
DLog(@"[健康知识] <<< 响应: %@", json);
|
|
if (completion) completion(json);
|
|
});
|
|
}];
|
|
[task resume];
|
|
}
|
|
|
|
@end
|