This commit is contained in:
2025-12-04 13:37:11 +08:00
parent f770f8055e
commit b216ddaa61
11 changed files with 377 additions and 10 deletions

View File

@@ -18,11 +18,21 @@
// 兼容旧命名(如有使用 API_APPLE_LOGIN 的位置,映射到统一命名)
#define API_APPLE_LOGIN @"/user/appleLogin" // Apple 登录
#define API_EMAIL_LOGIN @"/user/login" // email 登录
#define API_LOGOUT @"/user/logout" // 退出登录
#define API_UPDATA_INFO @"/user/updateInfo" // 更新用户
#define KB_API_USER_DETAIL @"/user/detail" // 用户详情
#define KB_API_CHARACTER_LIST @"/character/list" // 排行榜角色列表(综合)
#define KB_API_CHARACTER_LIST_BY_TAG @"/character/listByTag" // 根据 tagId 获取角色列表
#define KB_API_TAG_LIST @"/tag/list" // 排行榜标签列表
#define KB_API_FILE_UPLOAD @"/file/upload" // 上传头像
#define KB_API_CHARACTER_DETAIL @"/character/detail" // 人设详情
#define KB_API_CHARACTER_LISTBYUSER @"/character/listByUser" // 人设详情
// 应用配置
#ifndef KB_API_APP_CONFIG

View File

@@ -6,7 +6,11 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, UserSex) {
UserSexMan = 0, // 男
UserSexWeman = 1, // 女
UserSexTwoSex = 2, // 两性
};
@interface KBUser : NSObject
// 标识
@@ -14,12 +18,13 @@ NS_ASSUME_NONNULL_BEGIN
// 基本信息
@property (nonatomic, copy, nullable) NSString *nickName;
@property (nonatomic, copy, nullable) NSString *avatarUrl; // 头像 URL
@property (nonatomic, copy, nullable) NSString *gender; // 性别(后端可能返回 string/int统一转字符串存
/// 0
@property (nonatomic, assign) UserSex gender; // 性别(后端可能返回 string/int统一转字符串存
@property (nonatomic, copy, nullable) NSString *email;
/// 邮箱是否验证
@property (nonatomic, assign) BOOL emailVerified;
// 会话信息
// token
@property (nonatomic, copy, nullable) NSString *token; // token/access_token/accessToken
@end

View File

@@ -10,9 +10,26 @@
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"userId": @[@"uid"],
@"gender": @[@"gender", @"sex"],
};
}
+ (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property {
if ([property.name isEqualToString:@"gender"]) {
if ([oldValue isKindOfClass:[NSNumber class]]) {
NSInteger intValue = [(NSNumber *)oldValue integerValue];
if (intValue >= UserSexTwoSex && intValue <= UserSexMan) {
return @(intValue);
} else {
//
KBLOG(@"⚠️ 收到非法的userStatus值: %ld", (long)intValue);
return @(UserSexMan);
}
}
return @(UserSexMan);
}
return oldValue;
}
@end

View File

@@ -24,6 +24,14 @@ typedef void(^KBLoginCompletion)(BOOL success, NSError * _Nullable error);
- (void)signInWithAppleFromViewController:(UIViewController *)presenter
completion:(KBLoginCompletion)completion;
/// 邮箱登录
- (void)signInWithAppleFromViewController:(UIViewController *)presenter
completion:(KBLoginCompletion)completion;
/// 邮箱登录
- (void)emailLoginEmail:(NSString *)email password:(NSString *)password WithCompletion:(KBLoginCompletion)completion;
/// 是否已登录:由 KBAuthManager 判断(是否存在有效 token
- (BOOL)isLoggedIn;

View File

@@ -70,6 +70,35 @@
}];
}
///
- (void)emailLoginEmail:(NSString *)email password:(NSString *)password WithCompletion:(KBLoginCompletion)completion;
{
[KBHUD show];
NSMutableDictionary *params = [NSMutableDictionary dictionary];
if (email.length) params[@"email"] = email;
if (password.length) params[@"password"] = password;
//
[[KBNetworkManager shared] POST:API_EMAIL_LOGIN jsonBody:params headers:nil completion:^(NSDictionary * _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[KBHUD dismiss];
if (error) { if (completion) completion(NO, error); return; }
NSDictionary *dict = jsonOrData[@"data"];
KBUser *user = [KBUser mj_objectWithKeyValues:dict];
self.currentUser = user;
if (user.token.length == 0) {
if (completion) completion(NO, [NSError errorWithDomain:@"KBLogin" code:-2 userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"No token returned")}]);
return;
}
[[KBUserSessionManager shared] handleLoginSuccessWithUser:user];
completion(true,nil);
// App
// BOOL ok = [[KBAuthManager shared] saveAccessToken:user.token
// refreshToken:nil
// expiryDate:nil
// userIdentifier:cred.user];
// if (completion) completion(ok, ok ? nil : [NSError errorWithDomain:@"KBLogin" code:-3 userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Failed to save login state")}]);
}];
}
#pragma mark - Helpers
// token / access_token / accessToken data/user

View File

@@ -10,6 +10,7 @@
#import "UICollectionViewLeftAlignedLayout.h"
#import "KBMyKeyboardCell.h"
#import "KBAlert.h"
#import "KBMyVM.h"
///
static NSString * const kKBMyKeyboardCellId = @"kKBMyKeyboardCellId";
@@ -29,6 +30,7 @@ static NSString * const kKBMyKeyboardCellId = @"kKBMyKeyboardCellId";
//
@property (nonatomic, strong) NSMutableArray<NSMutableArray<NSDictionary *> *> *dataSourceArray; // {emoji,title}
@property (nonatomic, strong) KBMyVM *viewModel; // VM
@end
@@ -40,6 +42,7 @@ static NSString * const kKBMyKeyboardCellId = @"kKBMyKeyboardCellId";
- (void)viewDidLoad {
[super viewDidLoad];
self.viewModel = [[KBMyVM alloc] init];
self.view.backgroundColor = [UIColor colorWithHex:0xF6F8F9];
self.kb_navView.backgroundColor = [UIColor clearColor];
self.kb_titleLabel.text = @"My KeyBoard";
@@ -71,7 +74,11 @@ static NSString * const kKBMyKeyboardCellId = @"kKBMyKeyboardCellId";
}];
//
[self buildDefaultData];
// [self buildDefaultData];
__weak typeof(self) weakSelf = self;
[self.viewModel fetchCharacterListByUserWithCompletion:^(NSArray<KBCharacter *> * _Nonnull characterArray, NSError * _Nullable error) {
}];
}
- (void)viewWillAppear:(BOOL)animated {

View File

@@ -326,17 +326,26 @@
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14.0)) {
[picker dismissViewControllerAnimated:YES completion:nil];
PHPickerResult *first = results.firstObject; if (!first) return;
PHPickerResult *first = results.firstObject;
if (!first) return;
NSItemProvider *p = first.itemProvider;
if ([p canLoadObjectOfClass:UIImage.class]) {
__weak typeof(self) weakSelf = self;
[p loadObjectOfClass:UIImage.class completionHandler:^(__kindof id<NSItemProviderReading> _Nullable object, NSError * _Nullable error) {
UIImage *img = ([object isKindOfClass:UIImage.class] ? (UIImage *)object : nil);
if (!img) return;
// 线 UI
// 100KB
NSUInteger targetKB = 50;
NSData *compressedData = [weakSelf kb_compressImage:img targetKB:targetKB];
if (!compressedData) return;
UIImage *compressedImage = [UIImage imageWithData:compressedData];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *compressed = [weakSelf kb_compressImage:img maxPixel:512 quality:0.85];
weakSelf.avatarView.image = compressed;
weakSelf.avatarJPEGData = UIImageJPEGRepresentation(compressed, 0.85);
});
}];
}
@@ -378,6 +387,95 @@
UIImage *result = [UIImage imageWithData:jpeg] ?: scaled ?: image;
return result;
}
/// KB
/// 1. JPEG
/// 2.
- (nullable NSData *)kb_compressImage:(UIImage *)image targetKB:(NSUInteger)targetKB {
if (!image || targetKB == 0) { return nil; }
NSUInteger maxBytes = targetKB * 1024;
//
CGFloat compression = 0.9f;
CGFloat minCompression = 0.1f; //
NSData *data = UIImageJPEGRepresentation(image, compression);
if (!data) return nil;
//
if (data.length <= maxBytes) {
return data;
}
// 1)
while (data.length > maxBytes && compression > minCompression + 0.01f) {
compression -= 0.1f;
data = UIImageJPEGRepresentation(image, compression);
if (!data) return nil;
}
if (data.length <= maxBytes) {
return data;
}
// 2) ->
UIImage *currentImage = image;
//
NSInteger maxResizeCount = 6;
NSInteger resizeCount = 0;
while (data.length > maxBytes && resizeCount < maxResizeCount) {
resizeCount++;
// (maxBytes / ) *
CGFloat ratio = (CGFloat)maxBytes / (CGFloat)data.length;
// 0.8
ratio *= 0.8f;
if (ratio <= 0.0f) {
ratio = 0.5f; //
}
CGFloat scale = sqrt(ratio);
if (scale >= 1.0f) {
scale = 0.5f; //
}
CGSize newSize = CGSizeMake(currentImage.size.width * scale,
currentImage.size.height * scale);
if (newSize.width < 10 || newSize.height < 10) {
// break
break;
}
//
UIGraphicsBeginImageContextWithOptions(newSize, NO, currentImage.scale);
[currentImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (!resizedImage) {
break;
}
currentImage = resizedImage;
//
compression = 0.9f;
data = UIImageJPEGRepresentation(currentImage, compression);
if (!data) return nil;
//
while (data.length > maxBytes && compression > minCompression + 0.01f) {
compression -= 0.1f;
data = UIImageJPEGRepresentation(currentImage, compression);
if (!data) return nil;
}
}
//
return data;
}
- (KBMyVM *)myVM{
if (!_myVM) {

View File

@@ -6,18 +6,31 @@
//
#import <Foundation/Foundation.h>
#import "KBCharacter.h"
@class KBUser;
NS_ASSUME_NONNULL_BEGIN
typedef void(^KBMyUserDetailCompletion)(KBUser *_Nullable user, NSError *_Nullable error);
typedef void(^KBCharacterListCompletion)(NSArray<KBCharacter *> *characterArray, NSError *_Nullable error);
typedef void(^KBUpLoadAvatarCompletion)(BOOL success, NSError * _Nullable error);
typedef void(^KBUpdateUserInfoCompletion)(BOOL success, NSError * _Nullable error);
@interface KBMyVM : NSObject
/// 获取当前用户详情(/user/detail
- (void)fetchUserDetailWithCompletion:(KBMyUserDetailCompletion)completion;
/// 用户人设列表(/character/listByUser
- (void)fetchCharacterListByUserWithCompletion:(KBCharacterListCompletion)completion;
/// 上传头像
- (void)upLoadAvatarWithData:(NSData *)avatarData completion:(KBUpLoadAvatarCompletion)completion;
/// 更新用户信息
- (void)updateUserInfo:(KBUser *)user completion:(KBUpdateUserInfoCompletion)completion;
/// 退出登录
- (void)logout;
@end

View File

@@ -42,6 +42,72 @@
if (completion) completion(user, nil);
}];
}
- (void)fetchCharacterListByUserWithCompletion:(KBCharacterListCompletion)completion{
[[KBNetworkManager shared] GET:KB_API_CHARACTER_LISTBYUSER
parameters:nil
headers:nil
autoShowBusinessError:NO
completion:^(NSDictionary *jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[KBHUD dismiss];
if (error) {
NSString *msg = KBBizMessageFromJSONObject(jsonOrData) ?: error.localizedDescription ?: KBLocalized(@"Network error");
[KBHUD showInfo:msg];
if (completion) completion([NSArray new], error);
return;
}
id dataObj = jsonOrData[KBData] ?: jsonOrData[@"data"];
if (![dataObj isKindOfClass:[NSArray class]]) {
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidResponse
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid response")}];
[KBHUD showInfo:e.localizedDescription];
if (completion) completion([NSArray new], e);
return;
}
NSArray<KBCharacter *> *list = [KBCharacter mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj];
if (completion) completion(list, nil);
}];
}
///
- (void)upLoadAvatarWithData:(NSData *)avatarData completion:(KBUpLoadAvatarCompletion)completion{
[KBHUD show];
[[KBNetworkManager shared] uploadFile:KB_API_FILE_UPLOAD
fileData:avatarData
fileName:@"avatar.jpg"
mimeType:@"image/jpeg"
headers:nil
completion:^(NSDictionary * _Nullable json,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
[KBHUD dismiss];
if (error) {
NSLog(@"上传失败: %@", error);
return;
}
NSString *avImageString = json[@"data"];
// [weakSelf.avatarView kb_setImageURL:[NSURL URLWithString:avImageString] placeholder:KBPlaceholderImage];
NSLog(@"上传成功: %@", json);
}];
}
///
- (void)updateUserInfo:(KBUser *)user completion:(KBUpdateUserInfoCompletion)completion{
///
KBUser *localUser = [KBUserSessionManager shared].currentUser;
NSMutableDictionary *params = [NSMutableDictionary dictionary];
if (localUser.userId.length) params[@"uid"] = localUser.userId;
if (localUser.nickName.length) params[@"nickName"] = localUser.nickName;
params[@"gender"] = (NSInteger)localUser.gender;
if (localUser.avatarUrl.length) params[@"avatarUrl"] = localUser.avatarUrl;
}
- (void)logout{
[KBHUD show];
[[KBNetworkManager shared] GET:API_LOGOUT

View File

@@ -79,6 +79,28 @@ typedef void(^KBNetworkDataCompletion)(NSData *_Nullable data,
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
completion:(KBNetworkCompletion)completion;
/// 上传文件multipart/form-data表单字段名固定为 "file"
/// path: 例如 @"file/upload" 或 @"/file/upload"
/// fileData: 文件二进制数据(比如 UIImageJPEGRepresentation
/// fileName: 例如 @"avatar.jpg"
/// mimeType: 例如 @"image/jpeg"
- (nullable NSURLSessionDataTask *)uploadFile:(NSString *)path
fileData:(NSData *)fileData
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
autoShowBusinessError:(BOOL)autoShowBusinessError
completion:(KBNetworkCompletion)completion;
/// 便捷版本:默认 autoShowBusinessError = YES
- (nullable NSURLSessionDataTask *)uploadFile:(NSString *)path
fileData:(NSData *)fileData
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
completion:(KBNetworkCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -186,6 +186,98 @@ autoShowBusinessError:YES
return [self startDataTaskWithRequest:req completion:completion];
}
#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];
}
#pragma mark - Core
- (BOOL)ensureEnabled:(KBNetworkCompletion)completion {