This commit is contained in:
2025-11-13 19:07:59 +08:00
parent 5ec950cc61
commit 50163d02a7
9 changed files with 378 additions and 88 deletions

View File

@@ -0,0 +1,34 @@
//
// KBUser.h
// 登录模块-用户模型MJExtension 解析)
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBUser : NSObject
// 标识
@property (nonatomic, copy, nullable) NSString *userId; // id/user_id/uid
@property (nonatomic, copy, nullable) NSString *appleUserId; // 用 Apple 登录返回的 userID可选
// 基本信息
@property (nonatomic, copy, nullable) NSString *nickname;
@property (nonatomic, copy, nullable) NSString *avatar; // 头像 URL
@property (nonatomic, copy, nullable) NSString *gender; // 性别(后端可能返回 string/int统一转字符串存
@property (nonatomic, copy, nullable) NSString *mobile;
@property (nonatomic, copy, nullable) NSString *email;
// 会话信息
@property (nonatomic, copy, nullable) NSString *token; // token/access_token/accessToken
@property (nonatomic, copy, nullable) NSString *refreshToken; // refresh_token/refreshToken
@property (nonatomic, strong, nullable) NSDate *expiryDate; // 若后端返回过期时间,转为日期
/// 从后端返回(可能顶层或 data/user 嵌套)中解析用户模型。内部使用 MJExtension。
+ (instancetype)userFromResponseObject:(id)jsonObject;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,97 @@
//
// KBUser.m
//
#import "KBUser.h"
#import <MJExtension/MJExtension.h>
@implementation KBUser
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"userId": @[@"id", @"user_id", @"uid"],
@"appleUserId": @[@"appleUserId", @"apple_user_id", @"apple_userid", @"appleUserID"],
@"nickname": @[@"nickname", @"nick", @"name"],
@"avatar": @[@"avatar", @"avatar_url", @"head", @"headimg"],
@"gender": @[@"gender", @"sex"],
@"mobile": @[@"mobile", @"phone"],
@"email": @[@"email"],
@"token": @[@"token", @"access_token", @"accessToken"],
@"refreshToken": @[@"refresh_token", @"refreshToken"],
@"expiryDate": @[@"expire_at", @"expireAt", @"expires_at", @"expiresAt", @"expired_at"],
};
}
// // NSDate
- (void)setExpiryDate:(NSDate *)expiryDate { _expiryDate = expiryDate; }
+ (instancetype)userFromResponseObject:(id)jsonObject {
if (!jsonObject) return nil;
NSDictionary *dict = nil;
if ([jsonObject isKindOfClass:NSDictionary.class]) {
dict = (NSDictionary *)jsonObject;
} else if ([jsonObject isKindOfClass:NSData.class]) {
// JSON data -> dict
id obj = [NSJSONSerialization JSONObjectWithData:(NSData *)jsonObject options:0 error:NULL];
if ([obj isKindOfClass:NSDictionary.class]) dict = obj;
}
if (!dict) return nil;
// data.user data
NSDictionary *candidate = nil;
id data = dict[@"data"]; if ([data isKindOfClass:NSDictionary.class]) { candidate = data; }
id user = [candidate objectForKey:@"user"]; if (![user isKindOfClass:NSDictionary.class]) { user = dict[@"user"]; }
NSDictionary *userDict = ([user isKindOfClass:NSDictionary.class]) ? (NSDictionary *)user : (candidate ?: dict);
KBUser *u = [KBUser mj_objectWithKeyValues:userDict];
// token
if (u.token.length == 0) {
NSString *t = [self pickTokenFromDictionary:dict];
if (t.length) u.token = t;
}
//
id exp = userDict[@"expire_at"] ?: userDict[@"expireAt"] ?: userDict[@"expires_at"] ?: userDict[@"expiresAt"] ?: userDict[@"expired_at"];
if ([exp isKindOfClass:NSNumber.class]) {
// /> 10^11
NSTimeInterval ts = [(NSNumber *)exp doubleValue];
if (ts > 1e11) ts = ts / 1000.0;
u.expiryDate = [NSDate dateWithTimeIntervalSince1970:ts];
} else if ([exp isKindOfClass:NSString.class]) {
// ISO8601
NSDateFormatter *fmt = [NSDateFormatter new];
fmt.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
fmt.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZ";
NSDate *d = [fmt dateFromString:(NSString *)exp];
if (!d) {
//
NSTimeInterval ts = [(NSString *)exp doubleValue];
if (ts > 0) d = [NSDate dateWithTimeIntervalSince1970:ts];
}
if (d) u.expiryDate = d;
}
return u;
}
+ (NSString *)pickTokenFromDictionary:(NSDictionary *)dict {
if (![dict isKindOfClass:NSDictionary.class]) return nil;
NSString *(^pick)(NSDictionary *) = ^NSString *(NSDictionary *d) {
NSArray *keys = @[ @"token", @"access_token", @"accessToken" ];
for (NSString *k in keys) {
id v = d[k]; if ([v isKindOfClass:NSString.class] && ((NSString *)v).length > 0) return v;
}
return nil;
};
NSString *t = pick(dict); if (t.length) return t;
id data = dict[@"data"]; if ([data isKindOfClass:NSDictionary.class]) { t = pick(data); if (t.length) return t; }
id user = dict[@"user"]; if ([user isKindOfClass:NSDictionary.class]) { t = pick(user); if (t.length) return t; }
NSDictionary *d2 = dict[@"data"]; if ([d2 isKindOfClass:NSDictionary.class]) {
NSDictionary *session = d2[@"session"]; if ([session isKindOfClass:NSDictionary.class]) { t = pick(session); if (t.length) return t; }
}
return nil;
}
@end

View File

@@ -0,0 +1,32 @@
//
// KBLoginVM.h
// 登录相关的 ViewModel封装 Apple 登录与服务端校验、登录态落盘。
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class KBUser;
NS_ASSUME_NONNULL_BEGIN
/// 登录完成回调
typedef void(^KBLoginCompletion)(BOOL success, NSError * _Nullable error);
@interface KBLoginVM : NSObject
+ (instancetype)shared;
/// 最近一次登录/拉取到的用户信息(仅内存缓存),可选
@property (atomic, strong, readonly, nullable) KBUser *currentUser; // 最近一次解析得到的用户
/// 调起 Apple 登录并在服务端完成校验;成功后会将 token 写入共享钥匙串KBAuthManager作为“已登录”的判断依据。
- (void)signInWithAppleFromViewController:(UIViewController *)presenter
completion:(KBLoginCompletion)completion;
/// 是否已登录:由 KBAuthManager 判断(是否存在有效 token
- (BOOL)isLoggedIn;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,99 @@
//
// KBLoginVM.m
//
#import "KBLoginVM.h"
#import "AppleSignInManager.h"
#import "KBNetworkManager.h"
#import "KBAuthManager.h"
#import "KBAPI.h"
#import "KBUser.h"
@interface KBLoginVM ()
@property (atomic, strong, readwrite, nullable) KBUser *currentUser;
@end
@implementation KBLoginVM
+ (instancetype)shared {
static KBLoginVM *vm; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ vm = [KBLoginVM new]; });
return vm;
}
- (BOOL)isLoggedIn {
return [[KBAuthManager shared] isLoggedIn];
}
- (void)signInWithAppleFromViewController:(UIViewController *)presenter
completion:(KBLoginCompletion)completion {
// Apple
[[AppleSignInManager shared] signInFromViewController:presenter completion:^(ASAuthorizationAppleIDCredential * _Nullable credential, NSError * _Nullable error) {
if (error) { if (completion) completion(NO, error); return; }
if (![credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
if (completion) completion(NO, [NSError errorWithDomain:@"KBLogin" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"无效的登录凭证"}]);
return;
}
ASAuthorizationAppleIDCredential *cred = (ASAuthorizationAppleIDCredential *)credential;
// identityToken/authorizationCode 使 identityToken code
NSString *identityToken = cred.identityToken ? [[NSString alloc] initWithData:cred.identityToken encoding:NSUTF8StringEncoding] : nil;
NSString *authorizationCode = cred.authorizationCode ? [[NSString alloc] initWithData:cred.authorizationCode encoding:NSUTF8StringEncoding] : nil;
NSMutableDictionary *params = [NSMutableDictionary dictionary];
if (identityToken.length) params[@"code"] = identityToken;
if (authorizationCode.length) params[@"accessCode"] = authorizationCode; // 使
if (cred.user.length) params[@"userID"] = cred.user; //
//
[[KBNetworkManager shared] POST:API_APPLE_LOGIN jsonBody:params headers:nil completion:^(id _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) { if (completion) completion(NO, error); return; }
// JSON token data/user
// 使 MJExtension
KBUser *user = [KBUser userFromResponseObject:jsonOrData];
self.currentUser = user;
NSString *token = user.token ?: [self.class tokenFromResponseObject:jsonOrData];
if (token.length == 0) {
if (completion) completion(NO, [NSError errorWithDomain:@"KBLogin" code:-2 userInfo:@{NSLocalizedDescriptionKey: @"未返回 token"}]);
return;
}
// App
BOOL ok = [[KBAuthManager shared] saveAccessToken:token
refreshToken:nil
expiryDate:nil
userIdentifier:cred.user];
if (completion) completion(ok, ok ? nil : [NSError errorWithDomain:@"KBLogin" code:-3 userInfo:@{NSLocalizedDescriptionKey: @"保存登录态失败"}]);
}];
}];
}
#pragma mark - Helpers
// token / access_token / accessToken data/user
+ (NSString *)tokenFromResponseObject:(id)obj {
if (![obj isKindOfClass:[NSDictionary class]]) return nil;
NSDictionary *dict = (NSDictionary *)obj;
NSString *(^pick)(NSDictionary *) = ^NSString *(NSDictionary *d) {
NSArray *keys = @[ @"token", @"access_token", @"accessToken" ];
for (NSString *k in keys) {
id v = d[k]; if ([v isKindOfClass:NSString.class] && ((NSString *)v).length > 0) return v;
}
return nil;
};
NSString *t = pick(dict);
if (t.length) return t;
id data = dict[@"data"]; if ([data isKindOfClass:NSDictionary.class]) { t = pick(data); if (t.length) return t; }
id user = dict[@"user"]; if ([user isKindOfClass:NSDictionary.class]) { t = pick(user); if (t.length) return t; }
// token data.session.token
NSDictionary *d2 = dict[@"data"]; if ([d2 isKindOfClass:NSDictionary.class]) {
NSDictionary *session = d2[@"session"]; if ([session isKindOfClass:NSDictionary.class]) { t = pick(session); if (t.length) return t; }
}
return nil;
}
@end