611 lines
13 KiB
Markdown
611 lines
13 KiB
Markdown
# 小程序 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 加载。
|