feat: 新增我的动态信息结构和相关API请求逻辑

- 在DynamicsModels.swift中新增MyMomentInfo结构,专门用于处理/dynamic/getMyDynamic接口的响应数据。
- 更新MyMomentsResponse结构以使用MyMomentInfo,确保数据类型一致性。
- 在LoginModels.swift中重构IDLoginAPIRequest和EmailLoginRequest,优化queryParameters的实现方式,提升代码可读性。
- 在RecoverPasswordFeature中重构ResetPasswordRequest,优化queryParameters的实现方式,确保一致性。
- 在多个视图中添加调试信息,增强调试能力和用户体验。
- 更新Localizable.strings文件,新增动态列表为空时的提示信息,提升用户交互体验。
This commit is contained in:
edwinQQQ
2025-08-04 19:12:31 +08:00
parent fa544139c1
commit 99a53d7274
14 changed files with 289 additions and 49 deletions

View File

@@ -50,6 +50,8 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = yanaAPITests;
sourceTree = "<group>";
};
@@ -256,14 +258,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n";
@@ -277,14 +275,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n";

View File

@@ -241,12 +241,56 @@ struct PublishFeedData: Codable, Equatable {
// MARK: - API
/// - /dynamic/getMyDynamic
struct MyMomentInfo: Codable, Equatable, Sendable {
let content: String
let uid: Int
let publishTime: Int64
let type: Int
// MomentsInfo
func toMomentsInfo() -> MomentsInfo {
return MomentsInfo(
dynamicId: 0, // dynamicId
uid: uid,
nick: "", //
avatar: "", //
type: type,
content: content,
likeCount: 0, //
isLike: false, //
commentCount: 0, //
publishTime: Int(publishTime / 1000), //
worldId: 0, // worldId
status: 1, //
playCount: nil,
dynamicResList: nil,
gender: nil,
squareTop: nil,
topicTop: nil,
newUser: nil,
defUser: nil,
scene: nil,
userVipInfoVO: nil,
headwearPic: nil,
headwearEffect: nil,
headwearType: nil,
headwearName: nil,
headwearId: nil,
experLevelPic: nil,
charmLevelPic: nil,
isCustomWord: nil,
labelList: nil
)
}
}
///
struct MyMomentsResponse: Codable, Equatable, Sendable {
let code: Int
let message: String
let data: [MomentsInfo]?
let timestamp: Int?
let data: [MyMomentInfo]?
let timestamp: Int64?
}
struct GetMyDynamicRequest: APIRequestProtocol {

View File

@@ -77,10 +77,29 @@ struct IDLoginAPIRequest: APIRequestProtocol {
let endpoint = APIEndpoint.login.path // 使
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
// MARK: - Private Properties
private let phone: String
private let password: String
private let clientSecret: String
private let version: String
private let clientId: String
private let grantType: String
// MARK: - Computed Properties
var queryParameters: [String: String]? {
return [
"phone": phone,
"password": password,
"client_secret": clientSecret,
"version": version,
"client_id": clientId,
"grant_type": grantType
]
}
/// ID
/// - Parameters:
/// - phone: DESID/
@@ -90,14 +109,12 @@ struct IDLoginAPIRequest: APIRequestProtocol {
/// - clientId: ID"erban-client"
/// - grantType: "password"
init(phone: String, password: String, clientSecret: String = "uyzjdhds", version: String = "1", clientId: String = "erban-client", grantType: String = "password") {
self.queryParameters = [
"phone": phone,
"password": password,
"client_secret": clientSecret,
"version": version,
"client_id": clientId,
"grant_type": grantType
];
self.phone = phone
self.password = password
self.clientSecret = clientSecret
self.version = version
self.clientId = clientId
self.grantType = grantType
}
}
@@ -527,10 +544,29 @@ struct EmailLoginRequest: APIRequestProtocol {
let endpoint = APIEndpoint.login.path
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
// MARK: - Private Properties
private let email: String
private let code: String
private let clientSecret: String
private let version: String
private let clientId: String
private let grantType: String
// MARK: - Computed Properties
var queryParameters: [String: String]? {
return [
"email": email,
"code": code,
"client_secret": clientSecret,
"version": version,
"client_id": clientId,
"grant_type": grantType
]
}
///
/// - Parameters:
/// - email: DES
@@ -540,14 +576,12 @@ struct EmailLoginRequest: APIRequestProtocol {
/// - clientId: ID"erban-client"
/// - grantType: "email"
init(email: String, code: String, clientSecret: String = "uyzjdhds", version: String = "1", clientId: String = "erban-client", grantType: String = "email") {
self.queryParameters = [
"email": email,
"code": code,
"client_secret": clientSecret,
"version": version,
"client_id": clientId,
"grant_type": grantType
]
self.email = email
self.code = code
self.clientSecret = clientSecret
self.version = version
self.clientId = clientId
self.grantType = grantType
}
}
@@ -603,18 +637,25 @@ struct GetUserInfoRequest: APIRequestProtocol {
let endpoint = APIEndpoint.getUserInfo.path
let method: HTTPMethod = .GET
let includeBaseParameters = true
let queryParameters: [String: String]?
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
let shouldShowLoading: Bool = false // loading
let shouldShowError: Bool = false //
// MARK: - Private Properties
private let uid: String
// MARK: - Computed Properties
var queryParameters: [String: String]? {
return [
"uid": uid
]
}
///
/// - Parameter uid: ID
init(uid: String) {
self.queryParameters = [
"uid": uid
]
self.uid = uid
}
}

View File

@@ -5,17 +5,16 @@ enum AppEnvironment {
struct AppConfig {
static let current: AppEnvironment = {
#if DEBUG
return .development
#else
// #if DEBUG
// return .development
// #else
return .production
#endif
// #endif
}()
static var baseURL: String {
switch current {
case .development:
// return "http://192.168.10.211:8080"
return "http://beta.api.molistar.xyz"
case .production:
return "https://api.epartylive.com"

View File

@@ -112,24 +112,36 @@ struct FeedListFeature {
case .fetchFeeds:
state.isLoading = true
state.error = nil
debugInfoSync("🔄 FeedListFeature: 开始获取动态")
// API
return .run { [apiService] send in
await send(.fetchFeedsResponse(TaskResult {
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
debugInfoSync("📡 FeedListFeature: 发送请求: \(request.endpoint)")
debugInfoSync(" 参数: dynamicId=\(request.dynamicId), pageSize=\(request.pageSize)")
return try await apiService.request(request)
}))
}
case let .fetchFeedsResponse(.success(response)):
state.isLoading = false
debugInfoSync("✅ FeedListFeature: API 请求成功")
debugInfoSync(" 响应码: \(response.code)")
debugInfoSync(" 消息: \(response.message)")
debugInfoSync(" 数据数量: \(response.data?.dynamicList.count ?? 0)")
if let list = response.data?.dynamicList {
state.moments = list
state.error = nil
state.currentPage = 1
state.hasMore = (list.count >= 20)
debugInfoSync("✅ FeedListFeature: 数据加载成功")
debugInfoSync(" 动态数量: \(list.count)")
debugInfoSync(" 是否有更多: \(state.hasMore)")
} else {
state.moments = []
state.error = response.message
state.hasMore = false
debugErrorSync("❌ FeedListFeature: 数据为空")
debugErrorSync(" 错误消息: \(response.message)")
}
return .none
case let .fetchFeedsResponse(.failure(error)):
@@ -137,6 +149,8 @@ struct FeedListFeature {
state.moments = []
state.error = error.localizedDescription
state.hasMore = false
debugErrorSync("❌ FeedListFeature: API 请求失败")
debugErrorSync(" 错误: \(error.localizedDescription)")
return .none
case .editFeedButtonTapped:
state.isEditFeedPresented = true

View File

@@ -23,7 +23,17 @@ struct MainFeature {
init(accountModel: AccountModel? = nil) {
self.accountModel = accountModel
let uid = accountModel?.uid.flatMap { Int($0) } ?? 0
self.me = MeFeature.State(displayUID: uid > 0 ? uid : nil)
debugInfoSync("🏗️ MainFeature 初始化")
debugInfoSync(" accountModel.uid: \(accountModel?.uid ?? "nil")")
debugInfoSync(" 转换后的uid: \(uid)")
var meState = MeFeature.State(displayUID: uid > 0 ? uid : nil)
if uid > 0 {
meState.uid = uid // uiddisplayUID
}
self.me = meState
debugInfoSync(" meState.uid: \(meState.uid)")
debugInfoSync(" meState.displayUID: \(meState.displayUID ?? -1)")
debugInfoSync(" meState.effectiveUID: \(meState.effectiveUID)")
}
}
@@ -68,6 +78,7 @@ struct MainFeature {
if tab == .other, let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 {
if state.me.displayUID != uid {
state.me.displayUID = uid
state.me.uid = uid // uid
state.me.isFirstLoad = true
}
return .send(.me(.onAppear))
@@ -90,6 +101,7 @@ struct MainFeature {
if state.selectedTab == .other, let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
if state.me.displayUID != uid {
state.me.displayUID = uid
state.me.uid = uid // uid
state.me.isFirstLoad = true
}
return .send(.me(.onAppear))

View File

@@ -61,7 +61,9 @@ struct MeDynamicFeature: Reducer {
state.isLoadingMore = false
switch result {
case let .success(resp):
let newDynamics = resp.data ?? []
let myMoments = resp.data ?? []
// MyMomentInfo MomentsInfo
let newDynamics = myMoments.map { $0.toMomentsInfo() }
if state.page == 1 {
state.dynamics = newDynamics
} else {
@@ -80,11 +82,21 @@ struct MeDynamicFeature: Reducer {
private func fetchDynamics(uid: Int, page: Int, pageSize: Int) -> Effect<Action> {
let apiService = self.apiService
return .run { send in
debugInfoSync("🔄 MeDynamicFeature: 开始获取动态")
debugInfoSync(" UID: \(uid)")
debugInfoSync(" 页码: \(page)")
debugInfoSync(" 页大小: \(pageSize)")
do {
let req = GetMyDynamicRequest(fromUid: uid, uid: uid, page: page, pageSize: pageSize)
let resp = try await apiService.request(req)
debugInfoSync("✅ MeDynamicFeature: API 请求成功")
debugInfoSync(" 响应码: \(resp.code)")
debugInfoSync(" 消息: \(resp.message)")
debugInfoSync(" 数据数量: \(resp.data?.count ?? 0)")
await send(.fetchResponse(.success(resp)))
} catch {
debugErrorSync("❌ MeDynamicFeature: API 请求失败: \(error.localizedDescription)")
await send(.fetchResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription))))
}
}

View File

@@ -27,6 +27,10 @@ struct MeFeature {
init(displayUID: Int? = nil) {
self.displayUID = displayUID
// displayUIDniluid
if let displayUID = displayUID {
self.uid = displayUID
}
}
// ID
@@ -57,13 +61,20 @@ struct MeFeature {
switch action {
case .onAppear:
guard state.isFirstLoad else { return .none }
debugInfoSync("📱 MeFeature onAppear")
debugInfoSync(" isFirstLoad: \(state.isFirstLoad)")
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
state.isFirstLoad = false
return .send(.refresh)
case .refresh:
guard state.effectiveUID > 0 else { return .none }
debugInfoSync("🔄 MeFeature refresh")
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
state.isRefreshing = true
state.page = 1
state.hasMore = true
state.userInfoError = nil //
state.momentsError = nil //
return .merge(
fetchUserInfo(uid: state.effectiveUID),
fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
@@ -89,7 +100,49 @@ struct MeFeature {
state.isRefreshing = false
switch result {
case let .success(resp):
let newMoments = resp.data ?? []
let myMoments = resp.data ?? []
// MyMomentInfo MomentsInfo
let newMoments = myMoments.map { myMoment in
var momentsInfo = myMoment.toMomentsInfo()
//
if let userInfo = state.userInfo {
// 使
momentsInfo = MomentsInfo(
dynamicId: momentsInfo.dynamicId,
uid: momentsInfo.uid,
nick: userInfo.nick ?? userInfo.nickname ?? "未知用户",
avatar: userInfo.avatar ?? "",
type: momentsInfo.type,
content: momentsInfo.content,
likeCount: momentsInfo.likeCount,
isLike: momentsInfo.isLike,
commentCount: momentsInfo.commentCount,
publishTime: momentsInfo.publishTime,
worldId: momentsInfo.worldId,
status: momentsInfo.status,
playCount: momentsInfo.playCount,
dynamicResList: momentsInfo.dynamicResList,
gender: userInfo.gender,
squareTop: momentsInfo.squareTop,
topicTop: momentsInfo.topicTop,
newUser: userInfo.newUser,
defUser: userInfo.defUser,
scene: momentsInfo.scene,
userVipInfoVO: nil, // UserVipInfoVO UserVipInfo nil
headwearPic: userInfo.userHeadwear?.pic,
headwearEffect: userInfo.userHeadwear?.effect,
headwearType: userInfo.userHeadwear?.type,
headwearName: userInfo.userHeadwear?.headwearName,
headwearId: userInfo.userHeadwear?.headwearId,
experLevelPic: userInfo.userLevelVo?.experUrl,
charmLevelPic: userInfo.userLevelVo?.charmUrl,
isCustomWord: momentsInfo.isCustomWord,
labelList: momentsInfo.labelList
)
}
return momentsInfo
}
if state.page == 1 {
state.moments = newMoments
} else {
@@ -98,8 +151,14 @@ struct MeFeature {
state.hasMore = newMoments.count == state.pageSize
if state.hasMore { state.page += 1 }
state.momentsError = nil
debugInfoSync("✅ 我的动态加载成功")
debugInfoSync(" 加载数量: \(newMoments.count)")
debugInfoSync(" 总数量: \(state.moments.count)")
debugInfoSync(" 是否有更多: \(state.hasMore)")
case let .failure(error):
state.momentsError = error.localizedDescription
debugErrorSync("❌ 我的动态加载失败: \(error.localizedDescription)")
}
return .none
case .settingButtonTapped:
@@ -118,9 +177,16 @@ struct MeFeature {
private func fetchUserInfo(uid: Int) -> Effect<Action> {
.run { send in
debugInfoSync("👤 开始获取用户信息")
debugInfoSync(" UID: \(uid)")
if let userInfo = await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) {
debugInfoSync("✅ 用户信息获取成功")
debugInfoSync(" 昵称: \(userInfo.nick ?? userInfo.nickname ?? "未知")")
debugInfoSync(" 头像: \(userInfo.avatar ?? "")")
await send(.userInfoResponse(.success(userInfo)))
} else {
debugErrorSync("❌ 用户信息获取失败")
await send(.userInfoResponse(.failure(.noData)))
}
}
@@ -128,11 +194,28 @@ struct MeFeature {
private func fetchMoments(uid: Int, page: Int, pageSize: Int) -> Effect<Action> {
.run { send in
debugInfoSync("🔄 开始获取我的动态")
debugInfoSync(" UID: \(uid)")
debugInfoSync(" 页码: \(page)")
debugInfoSync(" 页大小: \(pageSize)")
do {
let req = GetMyDynamicRequest(fromUid: uid, uid: uid, page: page, pageSize: pageSize)
debugInfoSync("📡 发送请求: \(req.endpoint)")
debugInfoSync(" 参数: fromUid=\(uid), uid=\(uid), page=\(page), pageSize=\(pageSize)")
let resp = try await apiService.request(req)
debugInfoSync("✅ API 请求成功")
debugInfoSync(" 响应码: \(resp.code)")
debugInfoSync(" 消息: \(resp.message)")
debugInfoSync(" 数据数量: \(resp.data?.count ?? 0)")
await send(.momentsResponse(.success(resp)))
} catch {
debugErrorSync("❌ API 请求失败: \(error.localizedDescription)")
if let apiError = error as? APIError {
debugErrorSync(" API错误类型: \(apiError)")
}
await send(.momentsResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription))))
}
}

View File

@@ -210,21 +210,32 @@ struct ResetPasswordRequest: APIRequestProtocol {
let endpoint = "/acc/pwd/resetByEmail" // API
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
var bodyParameters: [String: Any]? { nil }
let timeout: TimeInterval = 30.0
// MARK: - Private Properties
private let email: String
private let code: String
private let newPwd: String
// MARK: - Computed Properties
var queryParameters: [String: String]? {
return [
"email": email,
"newPwd": newPwd, // newPwd
"code": code
]
}
///
/// - Parameters:
/// - email: DES
/// - code:
/// - newPwd: DES
init(email: String, code: String, newPwd: String) {
self.queryParameters = [
"email": email,
"newPwd": newPwd, // newPwd
"code": code
]
self.email = email
self.code = code
self.newPwd = newPwd
}
}

View File

@@ -100,6 +100,7 @@
// MARK: - Feed List
"feedList.title" = "Enjoy your Life Time";
"feedList.slogan" = "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.";
"feedList.empty" = "No moments yet";
// MARK: - Feed
"feed.title" = "Enjoy your Life Time";

View File

@@ -97,7 +97,8 @@
"editFeed.enterContent" = "输入内容";
"feedList.title" = "享受您的生活时光";
"feedList.slogan" = "疾病如同残酷的统治者,\n而时间是我们最宝贵的财富。\n我们活着的每一刻都是对不可避免命运的胜利。";
"feedList.slogan" = "疾病如同残酷的统治者,\n而时间是我们最宝贵的财富。\n我们活着的每一刻都是对不可避免命运的胜利。";
"feedList.empty" = "暂无动态";
"feed.title" = "享受您的生活时光";
"feed.empty" = "暂无动态内容";

View File

@@ -23,7 +23,7 @@ struct DetailView: View {
WithPerceptionTracking {
CustomNavigationBar(
title: LocalizedString("detail.title", comment: "Detail page title"),
showDeleteButton: isCurrentUserDynamic,
showDeleteButton: false,
isDeleteLoading: store.isDeleteLoading,
onBack: {
// onDismiss?() 使 dismiss()

View File

@@ -173,6 +173,13 @@ struct FeedListContentView: View {
@Binding var previewCurrentIndex: Int
var body: some View {
//
let _ = debugInfoSync("📱 FeedListContentView 状态:")
let _ = debugInfoSync(" isLoading: \(store.isLoading)")
let _ = debugInfoSync(" error: \(store.error ?? "nil")")
let _ = debugInfoSync(" moments.count: \(store.moments.count)")
let _ = debugInfoSync(" hasMore: \(store.hasMore)")
if store.isLoading {
FeedListLoadingView()
} else if let error = store.error {

View File

@@ -79,6 +79,11 @@ struct MeView: View {
}
}
.onAppear {
debugInfoSync("📱 MeView onAppear")
debugInfoSync(" 用户信息: \(store.userInfo?.nick ?? "nil")")
debugInfoSync(" 动态数量: \(store.moments.count)")
debugInfoSync(" 用户信息错误: \(store.userInfoError ?? "nil")")
debugInfoSync(" 动态错误: \(store.momentsError ?? "nil")")
store.send(.onAppear)
}
//
@@ -174,7 +179,7 @@ struct MeView: View {
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
Button("重试") {
store.send(.onAppear)
store.send(.refresh)
}
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
@@ -192,6 +197,22 @@ struct MeView: View {
Text("暂无动态")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white.opacity(0.7))
//
Text("调试: moments.count = \(store.moments.count)")
.font(.system(size: 12))
.foregroundColor(.yellow)
Text("调试: isLoadingUserInfo = \(store.isLoadingUserInfo)")
.font(.system(size: 12))
.foregroundColor(.yellow)
Text("调试: isLoadingMoments = \(store.isLoadingMoments)")
.font(.system(size: 12))
.foregroundColor(.yellow)
Text("调试: userInfoError = \(store.userInfoError ?? "nil")")
.font(.system(size: 12))
.foregroundColor(.yellow)
Text("调试: momentsError = \(store.momentsError ?? "nil")")
.font(.system(size: 12))
.foregroundColor(.yellow)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {