Files
e-party-iOS/yana/APIs/API dynamic feed rule.md
edwinQQQ 12bb4a5f8c feat: 更新Podfile和Podfile.lock,添加最新动态API文档和相关功能
- 在Podfile中添加Alamofire依赖,并更新Podfile.lock以反映更改。
- 新增动态内容API文档,详细描述`dynamic/square/latestDynamics`接口的请求参数、响应数据结构及示例。
- 实现动态内容的模型和API请求结构,支持获取最新动态列表。
- 更新FeedView和HomeView以集成动态内容展示,增强用户体验。
- 添加动态卡片组件,展示用户动态信息及互动功能。
2025-07-11 20:18:24 +08:00

16 KiB
Raw Blame History

dynamic/square/latestDynamics API 文档

概述

dynamic/square/latestDynamics 是获取朋友圈动态最新列表的 API 接口,用于获取用户动态内容的最新更新。

接口信息

属性
接口路径 GET /dynamic/square/latestDynamics
请求方法 GET
认证要求 需要 pub_uidpub_ticket
内容类型 application/json

请求参数

参数名 类型 必填 描述 示例值
dynamicId String 最新动态的ID用于分页加载。首次请求传空字符串 """123456"
pageSize String 每页返回的数据数量 "20"
types String 动态内容类型,多个类型用逗号分隔 "0,2"

types 参数说明

  • 0: 纯文字动态
  • 2: 图片动态

响应数据结构

成功响应 (200)

{
  "code": 200,
  "message": "success",
  "data": {
    "dynamicList": [
      {
        "dynamicId": "123456",
        "uid": "789012",
        "nick": "用户昵称",
        "avatar": "https://example.com/avatar.jpg",
        "gender": 1,
        "age": 25,
        "type": 0,
        "content": "动态内容文字",
        "likeCount": "15",
        "isLike": false,
        "commentCount": "3",
        "publishTime": "2024-01-15 10:30:00",
        "worldId": 456,
        "worldName": "话题名称",
        "squareTop": false,
        "topicTop": false,
        "newUser": false,
        "defUser": 0,
        "inRoomUid": "",
        "dynamicResList": [
          {
            "resUrl": "https://example.com/image.jpg",
            "format": "jpg",
            "width": 720,
            "height": 960
          }
        ],
        "userVipInfoVO": {
          "vipLevel": 3,
          "vipExpire": "2024-12-31"
        },
        "headwearPic": "https://example.com/headwear.png",
        "headwearEffect": "https://example.com/effect.svga",
        "headwearType": 1,
        "expertLevelPic": "https://example.com/expert_lv3.png",
        "charmLevelPic": "https://example.com/charm_lv2.png",
        "nameplatePic": "https://example.com/nameplate.png",
        "nameplateWord": "自定义铭牌",
        "isCustomWord": true,
        "labelList": ["新人", "活跃"]
      }
    ],
    "nextDynamicId": "123455"
  }
}

错误响应

{
  "code": 400,
  "message": "参数错误",
  "data": null
}

Swift 实现示例

1. 数据模型定义

// MARK: - 响应数据模型
struct MomentsLatestResponse: Codable {
    let code: Int
    let message: String
    let data: MomentsListData?
}

struct MomentsListData: Codable {
    let dynamicList: [MomentsInfo]
    let nextDynamicId: String
}

struct MomentsInfo: Codable {
    let dynamicId: String
    let uid: String
    let nick: String
    let avatar: String
    let gender: Int
    let age: Int
    let type: Int
    let content: String
    let likeCount: String
    let isLike: Bool
    let commentCount: String
    let publishTime: String
    let worldId: Int
    let worldName: String?
    let squareTop: Bool
    let topicTop: Bool
    let newUser: Bool
    let defUser: Int
    let inRoomUid: String?
    let dynamicResList: [MomentsPicture]?
    let userVipInfoVO: UserVipInfo?
    let headwearPic: String?
    let headwearEffect: String?
    let headwearType: Int?
    let expertLevelPic: String?
    let charmLevelPic: String?
    let nameplatePic: String?
    let nameplateWord: String?
    let isCustomWord: Bool?
    let labelList: [String]?
}

struct MomentsPicture: Codable {
    let resUrl: String
    let format: String
    let width: CGFloat
    let height: CGFloat
}

struct UserVipInfo: Codable {
    let vipLevel: Int
    let vipExpire: String?
}

// MARK: - 内容类型枚举
enum MomentsContentType: Int, CaseIterable {
    case text = 0      // 纯文字
    case picture = 2   // 图片
}

2. API 服务实现

import Foundation
import Combine

class MomentsAPIService {
    
    private let baseURL = "https://api.yourapp.com"
    private let session = URLSession.shared
    
    // MARK: - 获取最新动态列表
    func fetchLatestMoments(
        dynamicId: String = "",
        pageSize: Int = 20,
        types: [MomentsContentType] = [.text, .picture]
    ) -> AnyPublisher<MomentsListData, Error> {
        
        // 构建请求参数
        var components = URLComponents(string: "\(baseURL)/dynamic/square/latestDynamics")!
        components.queryItems = [
            URLQueryItem(name: "dynamicId", value: dynamicId),
            URLQueryItem(name: "pageSize", value: String(pageSize)),
            URLQueryItem(name: "types", value: types.map { String($0.rawValue) }.joined(separator: ","))
        ]
        
        guard let url = components.url else {
            return Fail(error: APIError.invalidURL)
                .eraseToAnyPublisher()
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        
        // 添加认证头
        if let uid = AuthManager.shared.currentUID {
            request.setValue(uid, forHTTPHeaderField: "pub_uid")
        }
        if let ticket = AuthManager.shared.currentTicket {
            request.setValue(ticket, forHTTPHeaderField: "pub_ticket")
        }
        
        // 添加其他公共头
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue(AppInfo.version, forHTTPHeaderField: "App-Version")
        request.setValue(Locale.current.languageCode ?? "en", forHTTPHeaderField: "Accept-Language")
        
        return session.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: MomentsLatestResponse.self, decoder: JSONDecoder())
            .compactMap { response in
                guard response.code == 200 else {
                    throw APIError.serverError(response.code, response.message)
                }
                return response.data
            }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

// MARK: - 错误类型定义
enum APIError: Error, LocalizedError {
    case invalidURL
    case noData
    case serverError(Int, String)
    case networkError(Error)
    
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "无效的URL"
        case .noData:
            return "无数据返回"
        case .serverError(let code, let message):
            return "服务器错误 (\(code)): \(message)"
        case .networkError(let error):
            return "网络错误: \(error.localizedDescription)"
        }
    }
}

3. ViewModel 实现

import Foundation
import Combine

@MainActor
class MomentsLatestViewModel: ObservableObject {
    
    @Published var moments: [MomentsInfo] = []
    @Published var isLoading = false
    @Published var hasMoreData = true
    @Published var errorMessage: String?
    
    private var nextDynamicId = ""
    private let apiService = MomentsAPIService()
    private var cancellables = Set<AnyCancellable>()
    
    // MARK: - 加载最新数据
    func loadLatestMoments() {
        loadMoments(isRefresh: true)
    }
    
    // MARK: - 加载更多数据
    func loadMoreMoments() {
        guard hasMoreData && !isLoading else { return }
        loadMoments(isRefresh: false)
    }
    
    // MARK: - 私有方法:统一加载逻辑
    private func loadMoments(isRefresh: Bool) {
        isLoading = true
        errorMessage = nil
        
        let dynamicId = isRefresh ? "" : nextDynamicId
        
        apiService.fetchLatestMoments(
            dynamicId: dynamicId,
            pageSize: 20,
            types: [.text, .picture]
        )
        .sink(
            receiveCompletion: { [weak self] completion in
                self?.isLoading = false
                if case .failure(let error) = completion {
                    self?.errorMessage = error.localizedDescription
                }
            },
            receiveValue: { [weak self] data in
                if isRefresh {
                    self?.moments = data.dynamicList
                } else {
                    self?.moments.append(contentsOf: data.dynamicList)
                }
                
                self?.nextDynamicId = data.nextDynamicId
                self?.hasMoreData = !data.dynamicList.isEmpty
            }
        )
        .store(in: &cancellables)
    }
    
    // MARK: - 点赞操作
    func toggleLike(for momentId: String) {
        // 实现点赞逻辑
        guard let index = moments.firstIndex(where: { $0.dynamicId == momentId }) else { return }
        
        moments[index] = MomentsInfo(
            dynamicId: moments[index].dynamicId,
            uid: moments[index].uid,
            nick: moments[index].nick,
            avatar: moments[index].avatar,
            gender: moments[index].gender,
            age: moments[index].age,
            type: moments[index].type,
            content: moments[index].content,
            likeCount: moments[index].isLike ? 
                String(max(0, Int(moments[index].likeCount) ?? 0 - 1)) :
                String((Int(moments[index].likeCount) ?? 0) + 1),
            isLike: !moments[index].isLike,
            commentCount: moments[index].commentCount,
            publishTime: moments[index].publishTime,
            worldId: moments[index].worldId,
            worldName: moments[index].worldName,
            squareTop: moments[index].squareTop,
            topicTop: moments[index].topicTop,
            newUser: moments[index].newUser,
            defUser: moments[index].defUser,
            inRoomUid: moments[index].inRoomUid,
            dynamicResList: moments[index].dynamicResList,
            userVipInfoVO: moments[index].userVipInfoVO,
            headwearPic: moments[index].headwearPic,
            headwearEffect: moments[index].headwearEffect,
            headwearType: moments[index].headwearType,
            expertLevelPic: moments[index].expertLevelPic,
            charmLevelPic: moments[index].charmLevelPic,
            nameplatePic: moments[index].nameplatePic,
            nameplateWord: moments[index].nameplateWord,
            isCustomWord: moments[index].isCustomWord,
            labelList: moments[index].labelList
        )
    }
}

4. SwiftUI 视图实现

import SwiftUI

struct MomentsLatestView: View {
    @StateObject private var viewModel = MomentsLatestViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.moments, id: \.dynamicId) { moment in
                    MomentCardView(moment: moment) {
                        viewModel.toggleLike(for: moment.dynamicId)
                    }
                    .onAppear {
                        // 当显示最后一个元素时加载更多
                        if moment.dynamicId == viewModel.moments.last?.dynamicId {
                            viewModel.loadMoreMoments()
                        }
                    }
                }
                
                if viewModel.isLoading {
                    HStack {
                        Spacer()
                        ProgressView()
                        Spacer()
                    }
                }
            }
            .refreshable {
                viewModel.loadLatestMoments()
            }
            .navigationTitle("最新动态")
            .onAppear {
                if viewModel.moments.isEmpty {
                    viewModel.loadLatestMoments()
                }
            }
            .alert("错误", isPresented: .constant(viewModel.errorMessage != nil)) {
                Button("确定") {
                    viewModel.errorMessage = nil
                }
            } message: {
                Text(viewModel.errorMessage ?? "")
            }
        }
    }
}

struct MomentCardView: View {
    let moment: MomentsInfo
    let onLike: () -> Void
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // 用户信息
            HStack {
                AsyncImage(url: URL(string: moment.avatar)) { image in
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                } placeholder: {
                    Circle()
                        .fill(Color.gray.opacity(0.3))
                }
                .frame(width: 40, height: 40)
                .clipShape(Circle())
                
                VStack(alignment: .leading) {
                    Text(moment.nick)
                        .font(.headline)
                    Text(moment.publishTime)
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                
                Spacer()
            }
            
            // 动态内容
            if !moment.content.isEmpty {
                Text(moment.content)
                    .font(.body)
            }
            
            // 图片内容
            if let pictures = moment.dynamicResList, !pictures.isEmpty {
                LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3)) {
                    ForEach(pictures.indices, id: \.self) { index in
                        AsyncImage(url: URL(string: pictures[index].resUrl)) { image in
                            image
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                        } placeholder: {
                            Rectangle()
                                .fill(Color.gray.opacity(0.3))
                        }
                        .frame(height: 100)
                        .clipped()
                    }
                }
            }
            
            // 操作栏
            HStack {
                Button(action: onLike) {
                    HStack {
                        Image(systemName: moment.isLike ? "heart.fill" : "heart")
                            .foregroundColor(moment.isLike ? .red : .gray)
                        Text(moment.likeCount)
                            .foregroundColor(.gray)
                    }
                }
                
                Spacer()
                
                HStack {
                    Image(systemName: "message")
                        .foregroundColor(.gray)
                    Text(moment.commentCount)
                        .foregroundColor(.gray)
                }
            }
        }
        .padding()
        .background(Color(.systemBackground))
        .cornerRadius(8)
    }
}

使用说明

基本用法

let viewModel = MomentsLatestViewModel()

// 加载最新数据
viewModel.loadLatestMoments()

// 加载更多数据
viewModel.loadMoreMoments()

分页逻辑

  • 首次请求:dynamicId 传空字符串
  • 后续分页:使用上次响应中的 nextDynamicId
  • 无更多数据:返回的 dynamicList 为空数组

错误处理

  • 网络错误:检查网络连接
  • 401 认证失败:重新登录获取 ticket
  • 其他服务器错误:显示具体错误信息

性能优化建议

  1. 使用图片缓存库(如 Kingfisher
  2. 实现虚拟列表避免内存过载
  3. 预加载下一页数据提升用户体验
  4. 实现本地缓存减少网络请求

注意事项

  1. 认证要求:所有请求必须包含有效的 pub_uidpub_ticket
  2. 参数验证pageSize 建议范围为 10-50
  3. 类型过滤types 参数支持多选,用逗号分隔
  4. 数据更新:推荐使用下拉刷新获取最新数据
  5. 错误重试:网络错误时实现自动重试机制