Files
soul-yongping/开发文档/8、部署/API接入说明.md
2026-02-09 15:09:29 +08:00

611 lines
13 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.

# 小程序 API 接入说明
## 📋 概述
将 newpp 项目从静态数据bookData.js改为从真实 API 加载数据。
---
## 🎯 接入的 API
### 1. 章节相关
| API | 方法 | 说明 | 参数 |
|-----|------|------|------|
| `/api/book/chapters` | GET | 获取章节列表 | partId, status, page, pageSize |
| `/api/book/chapter/[id]` | GET | 获取章节详情 | id路径参数 |
### 2. 用户相关
| API | 方法 | 说明 | 参数 |
|-----|------|------|------|
| `/api/user/profile` | GET | 获取用户信息 | userId, openId |
| `/api/user/profile` | POST | 更新用户信息 | userId, openId, nickname, avatar, phone, wechatId |
### 3. 配置相关
| API | 方法 | 说明 | 参数 |
|-----|------|------|------|
| `/api/db/config` | GET | 获取系统配置 | 无 |
| `/api/match/config` | GET | 获取找伙伴配置 | 无 |
### 4. 找伙伴相关
| API | 方法 | 说明 | 参数 |
|-----|------|------|------|
| `/api/ckb/join` | POST | 加入匹配池 | type, wechat, description |
| `/api/match/users` | GET | 获取匹配用户 | type |
### 5. 推广相关
| API | 方法 | 说明 | 参数 |
|-----|------|------|------|
| `/api/referral/data` | GET | 获取推广数据 | userId |
| `/api/referral/bind` | POST | 绑定推荐人 | userId, referralCode |
| `/api/referral/visit` | POST | 记录推广访问 | referralCode |
### 6. 搜索相关
| API | 方法 | 说明 | 参数 |
|-----|------|------|------|
| `/api/search` | GET | 搜索章节 | q关键词 |
### 7. 支付相关
| API | 方法 | 说明 | 参数 |
|-----|------|------|------|
| `/api/payment/create-order` | POST | 创建订单 | userId, type, sectionId, amount, payMethod |
| `/api/payment/status/[orderSn]` | GET | 查询订单状态 | orderSn路径参数 |
| `/api/payment/methods` | GET | 获取支付方式列表 | 无 |
### 8. 提现相关
| API | 方法 | 说明 | 参数 |
|-----|------|------|------|
| `/api/withdraw` | POST | 申请提现 | userId, amount, method, account, realName |
---
## 📁 文件结构
```
newpp/src/
├── api/
│ └── index.js # ✅ API 集成层(封装所有 API
├── hooks/
│ ├── useChapters.js # ✅ 章节列表 Hook
│ └── useChapterContent.js # ✅ 章节内容 Hook
├── adapters/
│ ├── request.js # ✅ 请求适配器(已有)
│ └── storage.js # ✅ 存储适配器(已有)
├── data/
│ └── bookData.js # ⚠️ 静态数据(待废弃)
└── pages/
├── HomePage.jsx # ⏳ 需要改用 useChapters
├── ChaptersPage.jsx # ⏳ 需要改用 useChapters
├── ReadPage.jsx # ⏳ 需要改用 useChapterContent
└── ...
```
---
## 🔧 核心实现
### 1. API 集成层
**文件**`newpp/src/api/index.js`
**作用**
- 封装所有 API 请求
- 统一处理错误和数据格式
- 提供类型化的接口
**示例**
```javascript
import { request } from '../adapters/request'
// 获取章节列表
export async function getChapters(params = {}) {
const { partId, status = 'published', page = 1, pageSize = 100 } = params
const query = new URLSearchParams({ status, page: String(page), pageSize: String(pageSize) })
if (partId) query.append('partId', partId)
const res = await request(`/api/book/chapters?${query.toString()}`)
return res
}
// 获取章节详情
export async function getChapterById(id) {
const res = await request(`/api/book/chapter/${id}`)
return res
}
```
---
### 2. 章节列表 Hook
**文件**`newpp/src/hooks/useChapters.js`
**功能**
1. ✅ 从 API 加载章节列表
2. ✅ 缓存到本地30分钟
3. ✅ 转换数据格式API → bookData
4. ✅ 提供辅助函数
**使用示例**
```javascript
import { useChapters } from '../hooks/useChapters'
export default function HomePage() {
const { bookData, loading, error, getTotalSectionCount, refresh } = useChapters()
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
const totalSections = getTotalSectionCount()
return (
<div>
<p> {totalSections} </p>
{bookData.map((part) => (
<div key={part.id}>
<h2>{part.title}</h2>
{/* ... */}
</div>
))}
</div>
)
}
```
---
### 3. 章节内容 Hook
**文件**`newpp/src/hooks/useChapterContent.js`
**功能**
1. ✅ 从 API 加载章节详情
2. ✅ 自动处理 loading 和 error
3. ✅ 支持重新加载
**使用示例**
```javascript
import { useChapterContent } from '../hooks/useChapterContent'
import { getPageQuery } from '../adapters/router'
export default function ReadPage() {
const { id } = getPageQuery()
const { content, loading, error, reload } = useChapterContent(id)
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
if (!content) return <div>章节不存在</div>
return (
<div>
<h1>{content.title}</h1>
<p>{content.words} </p>
<div dangerouslySetInnerHTML={{ __html: content.content }} />
</div>
)
}
```
---
## 🔄 数据转换
### API 返回格式
```json
{
"success": true,
"data": {
"list": [
{
"id": "1.1",
"part_id": "part-1",
"part_title": "真实的人",
"chapter_id": "chapter-1",
"chapter_title": "人与人之间的底层逻辑",
"section_title": "荷包:电动车出租的被动收入模式",
"content": "...",
"word_count": 1500,
"is_free": true,
"price": 0,
"sort_order": 1,
"status": "published"
}
],
"total": 50,
"page": 1,
"pageSize": 100,
"totalPages": 1
}
}
```
### bookData 格式
```javascript
[
{
id: 'part-1',
number: '01',
title: '真实的人',
subtitle: '人性观察与社交逻辑',
chapters: [
{
id: 'chapter-1',
title: '人与人之间的底层逻辑',
sections: [
{
id: '1.1',
title: '荷包:电动车出租的被动收入模式',
isFree: true,
price: 1,
wordCount: 1500,
}
]
}
]
}
]
```
### 转换函数
```javascript
function transformChapters(chapters) {
const partsMap = new Map()
chapters.forEach((item) => {
// 确保 part 存在
if (!partsMap.has(item.part_id)) {
partsMap.set(item.part_id, {
id: item.part_id,
number: item.part_id.replace('part-', '').padStart(2, '0'),
title: item.part_title,
subtitle: '',
chapters: []
})
}
const part = partsMap.get(item.part_id)
// 查找或创建 chapter
let chapter = part.chapters.find((c) => c.id === item.chapter_id)
if (!chapter) {
chapter = {
id: item.chapter_id,
title: item.chapter_title,
sections: []
}
part.chapters.push(chapter)
}
// 添加 section
chapter.sections.push({
id: item.id,
title: item.section_title,
isFree: item.is_free || false,
price: item.price || 1,
wordCount: item.word_count || 0,
})
})
return Array.from(partsMap.values())
}
```
---
## 📦 缓存策略
### 缓存位置
- **小程序**`wx.storage`
- **Web**`localStorage`
### 缓存时长
- **章节列表**30分钟
- **章节内容**:不缓存(内容可能更新)
### 缓存格式
```javascript
{
data: [...], // 数据
timestamp: 1706940000000 // 时间戳
}
```
### 缓存逻辑
```javascript
// 1. 尝试从缓存加载
const cached = await storage.getItem(CACHE_KEY)
if (cached) {
const { data, timestamp } = JSON.parse(cached)
if (Date.now() - timestamp < CACHE_DURATION) {
setBookData(data)
return
}
}
// 2. 从 API 加载
const res = await getChapters({ status: 'published', pageSize: 1000 })
const transformed = transformChapters(res.data.list)
setBookData(transformed)
// 3. 缓存数据
await storage.setItem(CACHE_KEY, JSON.stringify({
data: transformed,
timestamp: Date.now()
}))
```
---
## 🔄 迁移步骤
### Phase 1创建 API 层 ✅
- [x] 创建 `api/index.js`
- [x] 创建 `hooks/useChapters.js`
- [x] 创建 `hooks/useChapterContent.js`
### Phase 2更新页面组件
#### 2.1 HomePage.jsx
**Before**
```javascript
import { getTotalSectionCount, bookData } from '../data/bookData'
const totalSections = getTotalSectionCount()
```
**After**
```javascript
import { useChapters } from '../hooks/useChapters'
export default function HomePage() {
const { bookData, loading, getTotalSectionCount } = useChapters()
if (loading) return <LoadingSpinner />
const totalSections = getTotalSectionCount()
// ...
}
```
#### 2.2 ChaptersPage.jsx
**Before**
```javascript
import { bookData } from '../data/bookData'
```
**After**
```javascript
import { useChapters } from '../hooks/useChapters'
export default function ChaptersPage() {
const { bookData, loading } = useChapters()
if (loading) return <LoadingSpinner />
// ...
}
```
#### 2.3 ReadPage.jsx
**Before**
```javascript
import { getSectionById } from '../data/bookData'
const section = getSectionById(id)
```
**After**
```javascript
import { useChapterContent } from '../hooks/useChapterContent'
import { getPageQuery } from '../adapters/router'
export default function ReadPage() {
const { id } = getPageQuery()
const { content, loading } = useChapterContent(id)
if (loading) return <LoadingSpinner />
if (!content) return <NotFound />
// ...
}
```
#### 2.4 SearchPage.jsx
**Before**
```javascript
import { getAllSections } from '../data/bookData'
const results = getAllSections().filter(s => s.title.includes(keyword))
```
**After**
```javascript
import { searchChapters } from '../api'
export default function SearchPage() {
const [results, setResults] = useState([])
const handleSearch = async (keyword) => {
const res = await searchChapters(keyword)
setResults(res.data || [])
}
// ...
}
```
### Phase 3集成到 Zustand Store
```javascript
// store/index.js
import { getChapters } from '../api'
const useStore = create(
persist(
(set, get) => ({
// ... 其他状态
// ✅ 添加章节数据
bookData: [],
loadChapters: async () => {
const res = await getChapters({ status: 'published', pageSize: 1000 })
if (res.success) {
set({ bookData: transformChapters(res.data.list) })
}
},
}),
{
name: 'soul-party-storage',
storage: {/* ... */},
}
)
)
```
### Phase 4移除静态数据
- [ ] 删除或重命名 `data/bookData.js`
- [ ] 更新所有导入路径
---
## 🐛 错误处理
### API 请求失败
```javascript
try {
const res = await getChapters()
if (!res.success) {
throw new Error(res.error || '请求失败')
}
} catch (err) {
console.error('加载失败:', err)
setError(err.message)
// ✅ 降级策略:使用缓存数据
const cached = await storage.getItem(CACHE_KEY)
if (cached) {
const { data } = JSON.parse(cached)
setBookData(data)
}
}
```
### 网络超时
```javascript
// adapters/request.js
export function request(url, options = {}) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000) // 10秒超时
return fetch(fullUrl, {
...options,
signal: controller.signal,
})
.finally(() => clearTimeout(timeout))
}
```
---
## 📊 性能优化
### 1. 缓存策略
- ✅ 章节列表缓存 30 分钟
- ✅ 减少 API 调用次数
- ✅ 提升加载速度
### 2. 懒加载
```javascript
// 只在需要时加载章节内容
useEffect(() => {
if (visible) {
loadContent()
}
}, [visible])
```
### 3. 预加载
```javascript
// 预加载下一章内容
useEffect(() => {
if (content && nextChapterId) {
// 延迟 2 秒预加载
const timer = setTimeout(() => {
getChapterById(nextChapterId)
}, 2000)
return () => clearTimeout(timer)
}
}, [content, nextChapterId])
```
---
## 🧪 测试清单
### API 集成测试
- [ ] 章节列表加载成功
- [ ] 章节详情加载成功
- [ ] 用户信息获取成功
- [ ] 配置加载成功
- [ ] 搜索功能正常
- [ ] 错误处理正确
### 缓存测试
- [ ] 首次加载从 API 获取
- [ ] 第二次加载从缓存读取
- [ ] 缓存过期后重新加载
- [ ] 缓存数据格式正确
### 跨平台测试
- [ ] Web 环境正常
- [ ] 小程序环境正常
- [ ] 数据格式一致
---
## 📚 相关文档
1. [API 集成层代码](../newpp/src/api/index.js)
2. [章节列表 Hook](../newpp/src/hooks/useChapters.js)
3. [章节内容 Hook](../newpp/src/hooks/useChapterContent.js)
---
**总结**API 集成层已完成,接下来需要更新各个页面组件,将静态数据改为从 API 加载。