JXMemberService.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. //
  2. // JXMemberService.m
  3. // AICity
  4. //
  5. // Feature: 003-ios-api-https
  6. // Task: T051 - iOS会员状态检查
  7. // Created on 2025-10-14.
  8. //
  9. #import "JXMemberService.h"
  10. #import "JXNetworkManager.h"
  11. // 通知名称
  12. NSString * const JXMemberStatusDidChangeNotification = @"JXMemberStatusDidChangeNotification";
  13. // 缓存超时时间(5分钟)
  14. static const NSTimeInterval kCacheTimeout = 5 * 60;
  15. @implementation JXMemberStatus
  16. - (BOOL)isExpired {
  17. return self.expireAt <= [[NSDate date] timeIntervalSince1970];
  18. }
  19. - (BOOL)isVip {
  20. return ![self.localMemberType isEqualToString:@"free"] && ![self isExpired];
  21. }
  22. - (NSInteger)getRemainingDays {
  23. if ([self isExpired]) return 0;
  24. NSTimeInterval remainingSeconds = self.expireAt - [[NSDate date] timeIntervalSince1970];
  25. return (NSInteger)(remainingSeconds / (24 * 60 * 60));
  26. }
  27. - (NSString *)getDisplayName {
  28. if ([self.localMemberType isEqualToString:@"free"]) {
  29. return @"免费用户";
  30. } else if ([self.localMemberType isEqualToString:@"basic"]) {
  31. return @"基础会员";
  32. } else if ([self.localMemberType isEqualToString:@"premium"]) {
  33. return @"高级会员";
  34. } else if ([self.localMemberType isEqualToString:@"vip"]) {
  35. return @"VIP会员";
  36. } else {
  37. return @"未知类型";
  38. }
  39. }
  40. @end
  41. @implementation JXAccessCheckResult
  42. @end
  43. @interface JXMemberService ()
  44. @property (nonatomic, strong) JXMemberStatus *currentMemberStatus;
  45. @property (nonatomic, strong) NSMutableDictionary<NSString *, JXAccessCheckResult *> *accessCache;
  46. @property (nonatomic, strong) JXNetworkManager *networkManager;
  47. @end
  48. @implementation JXMemberService
  49. #pragma mark - Singleton
  50. + (instancetype)sharedService {
  51. static JXMemberService *sharedInstance = nil;
  52. static dispatch_once_t onceToken;
  53. dispatch_once(&onceToken, ^{
  54. sharedInstance = [[self alloc] init];
  55. });
  56. return sharedInstance;
  57. }
  58. - (instancetype)init {
  59. self = [super init];
  60. if (self) {
  61. _accessCache = [[NSMutableDictionary alloc] init];
  62. _networkManager = [JXNetworkManager sharedManager];
  63. }
  64. return self;
  65. }
  66. #pragma mark - Public Methods
  67. - (void)getCurrentMemberStatusWithForceRefresh:(BOOL)forceRefresh
  68. completion:(void(^)(JXMemberStatus * _Nullable memberStatus, NSError * _Nullable error))completion {
  69. // 如果有缓存且不强制刷新,返回缓存
  70. if (!forceRefresh && self.currentMemberStatus && ![self isMemberStatusExpired:self.currentMemberStatus]) {
  71. if (completion) {
  72. completion(self.currentMemberStatus, nil);
  73. }
  74. return;
  75. }
  76. // 从服务器获取最新状态
  77. [self.networkManager getMemberStatusWithCompletion:^(NSDictionary * _Nullable response, NSError * _Nullable error) {
  78. if (error) {
  79. // 网络异常时返回缓存的状态
  80. if (self.currentMemberStatus) {
  81. if (completion) {
  82. completion(self.currentMemberStatus, nil);
  83. }
  84. } else {
  85. if (completion) {
  86. completion(nil, error);
  87. }
  88. }
  89. return;
  90. }
  91. // 解析响应数据
  92. if (response && [response[@"code"] integerValue] == 0 && response[@"data"]) {
  93. JXMemberStatus *memberStatus = [self parseMemberStatusFromResponse:response[@"data"]];
  94. self.currentMemberStatus = memberStatus;
  95. // 清除过期的权限缓存
  96. [self clearExpiredAccessCache];
  97. // 发送状态变更通知
  98. [[NSNotificationCenter defaultCenter] postNotificationName:JXMemberStatusDidChangeNotification
  99. object:memberStatus];
  100. if (completion) {
  101. completion(memberStatus, nil);
  102. }
  103. } else {
  104. NSString *message = response[@"message"] ?: @"获取会员状态失败";
  105. NSError *apiError = [NSError errorWithDomain:@"JXMemberService"
  106. code:-1
  107. userInfo:@{NSLocalizedDescriptionKey: message}];
  108. if (completion) {
  109. completion(nil, apiError);
  110. }
  111. }
  112. }];
  113. }
  114. - (void)checkContentAccessWithContentType:(NSString *)contentType
  115. requiredLevel:(NSInteger)requiredLevel
  116. contentId:(nullable NSString *)contentId
  117. completion:(void(^)(JXAccessCheckResult * _Nullable result, NSError * _Nullable error))completion {
  118. NSString *cacheKey = [NSString stringWithFormat:@"%@_%ld_%@", contentType, (long)requiredLevel, contentId ?: @"default"];
  119. // 检查缓存
  120. JXAccessCheckResult *cached = self.accessCache[cacheKey];
  121. if (cached && ![self isAccessCacheExpired:cached]) {
  122. if (completion) {
  123. completion(cached, nil);
  124. }
  125. return;
  126. }
  127. // 获取会员状态
  128. [self getCurrentMemberStatusWithForceRefresh:NO completion:^(JXMemberStatus * _Nullable memberStatus, NSError * _Nullable error) {
  129. if (error || !memberStatus) {
  130. if (completion) {
  131. completion(nil, error ?: [NSError errorWithDomain:@"JXMemberService"
  132. code:-1
  133. userInfo:@{NSLocalizedDescriptionKey: @"获取会员状态失败"}]);
  134. }
  135. return;
  136. }
  137. // 本地权限检查
  138. BOOL hasAccess = memberStatus.accessLevel >= requiredLevel;
  139. JXAccessCheckResult *result = [[JXAccessCheckResult alloc] init];
  140. result.hasAccess = hasAccess;
  141. result.memberStatus = memberStatus;
  142. result.contentType = contentType;
  143. result.requiredLevel = requiredLevel;
  144. result.contentId = contentId;
  145. result.checkTime = [[NSDate date] timeIntervalSince1970];
  146. result.reason = hasAccess ? @"权限验证通过" : @"会员等级不足";
  147. // 缓存结果
  148. self.accessCache[cacheKey] = result;
  149. if (completion) {
  150. completion(result, nil);
  151. }
  152. }];
  153. }
  154. - (void)checkEpisodeAccessWithEpisodeId:(NSString *)episodeId
  155. requiredLevel:(NSInteger)requiredLevel
  156. completion:(void(^)(JXAccessCheckResult * _Nullable result, NSError * _Nullable error))completion {
  157. [self checkContentAccessWithContentType:@"episode"
  158. requiredLevel:requiredLevel
  159. contentId:episodeId
  160. completion:completion];
  161. }
  162. - (void)checkDramaAccessWithDramaId:(NSString *)dramaId
  163. requiredLevel:(NSInteger)requiredLevel
  164. completion:(void(^)(JXAccessCheckResult * _Nullable result, NSError * _Nullable error))completion {
  165. [self checkContentAccessWithContentType:@"drama"
  166. requiredLevel:requiredLevel
  167. contentId:dramaId
  168. completion:completion];
  169. }
  170. - (void)syncMemberStatusWithCompletion:(void(^)(BOOL success, NSError * _Nullable error))completion {
  171. [self.networkManager syncMemberStatusWithCompletion:^(NSDictionary * _Nullable response, NSError * _Nullable error) {
  172. if (error) {
  173. if (completion) {
  174. completion(NO, error);
  175. }
  176. return;
  177. }
  178. if (response && [response[@"code"] integerValue] == 0) {
  179. // 同步成功后刷新本地状态
  180. [self getCurrentMemberStatusWithForceRefresh:YES completion:^(JXMemberStatus * _Nullable memberStatus, NSError * _Nullable error) {
  181. if (completion) {
  182. completion(error == nil, error);
  183. }
  184. }];
  185. } else {
  186. NSString *message = response[@"message"] ?: @"同步会员状态失败";
  187. NSError *apiError = [NSError errorWithDomain:@"JXMemberService"
  188. code:-1
  189. userInfo:@{NSLocalizedDescriptionKey: message}];
  190. if (completion) {
  191. completion(NO, apiError);
  192. }
  193. }
  194. }];
  195. }
  196. - (void)isVipUserWithCompletion:(void(^)(BOOL isVip, NSError * _Nullable error))completion {
  197. [self getCurrentMemberStatusWithForceRefresh:NO completion:^(JXMemberStatus * _Nullable memberStatus, NSError * _Nullable error) {
  198. if (error) {
  199. if (completion) {
  200. completion(NO, error);
  201. }
  202. return;
  203. }
  204. BOOL isVip = memberStatus && [memberStatus isVip];
  205. if (completion) {
  206. completion(isVip, nil);
  207. }
  208. }];
  209. }
  210. - (BOOL)isMemberExpired:(JXMemberStatus *)memberStatus {
  211. return [memberStatus isExpired];
  212. }
  213. - (void)getMemberExpireTimeWithCompletion:(void(^)(NSTimeInterval expireTime, NSError * _Nullable error))completion {
  214. [self getCurrentMemberStatusWithForceRefresh:NO completion:^(JXMemberStatus * _Nullable memberStatus, NSError * _Nullable error) {
  215. if (error) {
  216. if (completion) {
  217. completion(0, error);
  218. }
  219. return;
  220. }
  221. NSTimeInterval expireTime = memberStatus ? memberStatus.expireAt : 0;
  222. if (completion) {
  223. completion(expireTime, nil);
  224. }
  225. }];
  226. }
  227. - (NSString *)getMemberTypeDisplayName:(NSString *)memberType {
  228. if ([memberType isEqualToString:@"free"]) {
  229. return @"免费用户";
  230. } else if ([memberType isEqualToString:@"basic"]) {
  231. return @"基础会员";
  232. } else if ([memberType isEqualToString:@"premium"]) {
  233. return @"高级会员";
  234. } else if ([memberType isEqualToString:@"vip"]) {
  235. return @"VIP会员";
  236. } else {
  237. return @"未知类型";
  238. }
  239. }
  240. - (NSString *)getAccessLevelDisplayName:(NSInteger)accessLevel {
  241. switch (accessLevel) {
  242. case 0:
  243. return @"免费内容";
  244. case 1:
  245. return @"付费内容";
  246. case 2:
  247. return @"高级内容";
  248. case 3:
  249. return @"VIP内容";
  250. default:
  251. return @"未知级别";
  252. }
  253. }
  254. - (void)clearMemberStatusCache {
  255. self.currentMemberStatus = nil;
  256. [self.accessCache removeAllObjects];
  257. }
  258. - (void)updateMemberStatus:(JXMemberStatus *)memberStatus {
  259. self.currentMemberStatus = memberStatus;
  260. // 清除权限缓存,强制重新检查
  261. [self.accessCache removeAllObjects];
  262. // 发送状态变更通知
  263. [[NSNotificationCenter defaultCenter] postNotificationName:JXMemberStatusDidChangeNotification
  264. object:memberStatus];
  265. }
  266. - (void)addMemberStatusObserver:(id)observer selector:(SEL)selector {
  267. [[NSNotificationCenter defaultCenter] addObserver:observer
  268. selector:selector
  269. name:JXMemberStatusDidChangeNotification
  270. object:nil];
  271. }
  272. - (void)removeMemberStatusObserver:(id)observer {
  273. [[NSNotificationCenter defaultCenter] removeObserver:observer
  274. name:JXMemberStatusDidChangeNotification
  275. object:nil];
  276. }
  277. #pragma mark - Private Methods
  278. - (JXMemberStatus *)parseMemberStatusFromResponse:(NSDictionary *)data {
  279. JXMemberStatus *memberStatus = [[JXMemberStatus alloc] init];
  280. memberStatus.userId = [data[@"user_id"] integerValue];
  281. memberStatus.localMemberType = data[@"local_member_type"] ?: @"free";
  282. memberStatus.juXingMemberType = data[@"juxing_member_type"] ?: @"0";
  283. memberStatus.accessLevel = [data[@"access_level"] integerValue];
  284. memberStatus.expireAt = [data[@"expire_at"] doubleValue];
  285. memberStatus.syncStatus = data[@"sync_status"] ?: @"pending";
  286. memberStatus.lastSyncAt = [data[@"last_sync_at"] doubleValue];
  287. memberStatus.retryCount = [data[@"retry_count"] integerValue];
  288. memberStatus.createdAt = [data[@"created_at"] doubleValue];
  289. memberStatus.updatedAt = [data[@"updated_at"] doubleValue];
  290. return memberStatus;
  291. }
  292. - (BOOL)isMemberStatusExpired:(JXMemberStatus *)memberStatus {
  293. // 缓存5分钟
  294. NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
  295. return currentTime - memberStatus.updatedAt > kCacheTimeout;
  296. }
  297. - (BOOL)isAccessCacheExpired:(JXAccessCheckResult *)result {
  298. NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
  299. return currentTime - result.checkTime > kCacheTimeout;
  300. }
  301. - (void)clearExpiredAccessCache {
  302. NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
  303. NSMutableArray *keysToRemove = [[NSMutableArray alloc] init];
  304. for (NSString *key in self.accessCache.allKeys) {
  305. JXAccessCheckResult *result = self.accessCache[key];
  306. if (currentTime - result.checkTime > kCacheTimeout) {
  307. [keysToRemove addObject:key];
  308. }
  309. }
  310. for (NSString *key in keysToRemove) {
  311. [self.accessCache removeObjectForKey:key];
  312. }
  313. }
  314. @end