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

521 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# **dynamic/square/latestDynamics API 文档**
## **概述**
`dynamic/square/latestDynamics` 是获取朋友圈动态最新列表的 API 接口,用于获取用户动态内容的最新更新。
## **接口信息**
| 属性 | 值 |
|------|-----|
| **接口路径** | `GET /dynamic/square/latestDynamics` |
| **请求方法** | `GET` |
| **认证要求** | 需要 `pub_uid``pub_ticket` |
| **内容类型** | `application/json` |
## **请求参数**
| 参数名 | 类型 | 必填 | 描述 | 示例值 |
|--------|------|------|------|--------|
| `dynamicId` | `String` | 否 | 最新动态的ID用于分页加载。首次请求传空字符串 | `""``"123456"` |
| `pageSize` | `String` | 是 | 每页返回的数据数量 | `"20"` |
| `types` | `String` | 是 | 动态内容类型,多个类型用逗号分隔 | `"0,2"` |
### **types 参数说明**
- `0`: 纯文字动态
- `2`: 图片动态
## **响应数据结构**
### **成功响应 (200)**
```json
{
"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"
}
}
```
### **错误响应**
```json
{
"code": 400,
"message": "参数错误",
"data": null
}
```
## **Swift 实现示例**
### **1. 数据模型定义**
```swift
// 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 服务实现**
```swift
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 实现**
```swift
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 视图实现**
```swift
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)
}
}
```
## **使用说明**
### **基本用法**
```swift
let viewModel = MomentsLatestViewModel()
// 加载最新数据
viewModel.loadLatestMoments()
// 加载更多数据
viewModel.loadMoreMoments()
```
### **分页逻辑**
- 首次请求:`dynamicId` 传空字符串
- 后续分页:使用上次响应中的 `nextDynamicId`
- 无更多数据:返回的 `dynamicList` 为空数组
### **错误处理**
- 网络错误:检查网络连接
- 401 认证失败:重新登录获取 ticket
- 其他服务器错误:显示具体错误信息
### **性能优化建议**
1. 使用图片缓存库(如 Kingfisher
2. 实现虚拟列表避免内存过载
3. 预加载下一页数据提升用户体验
4. 实现本地缓存减少网络请求
## **注意事项**
1. **认证要求**:所有请求必须包含有效的 `pub_uid``pub_ticket`
2. **参数验证**`pageSize` 建议范围为 10-50
3. **类型过滤**`types` 参数支持多选,用逗号分隔
4. **数据更新**:推荐使用下拉刷新获取最新数据
5. **错误重试**:网络错误时实现自动重试机制