13 KiB
13 KiB
小程序 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 请求
- 统一处理错误和数据格式
- 提供类型化的接口
示例:
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
功能:
- ✅ 从 API 加载章节列表
- ✅ 缓存到本地(30分钟)
- ✅ 转换数据格式(API → bookData)
- ✅ 提供辅助函数
使用示例:
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
功能:
- ✅ 从 API 加载章节详情
- ✅ 自动处理 loading 和 error
- ✅ 支持重新加载
使用示例:
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 返回格式
{
"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 格式
[
{
id: 'part-1',
number: '01',
title: '真实的人',
subtitle: '人性观察与社交逻辑',
chapters: [
{
id: 'chapter-1',
title: '人与人之间的底层逻辑',
sections: [
{
id: '1.1',
title: '荷包:电动车出租的被动收入模式',
isFree: true,
price: 1,
wordCount: 1500,
}
]
}
]
}
]
转换函数
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分钟
- 章节内容:不缓存(内容可能更新)
缓存格式
{
data: [...], // 数据
timestamp: 1706940000000 // 时间戳
}
缓存逻辑
// 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 层 ✅
- 创建
api/index.js - 创建
hooks/useChapters.js - 创建
hooks/useChapterContent.js
Phase 2:更新页面组件
2.1 HomePage.jsx
Before:
import { getTotalSectionCount, bookData } from '../data/bookData'
const totalSections = getTotalSectionCount()
After:
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:
import { bookData } from '../data/bookData'
After:
import { useChapters } from '../hooks/useChapters'
export default function ChaptersPage() {
const { bookData, loading } = useChapters()
if (loading) return <LoadingSpinner />
// ...
}
2.3 ReadPage.jsx
Before:
import { getSectionById } from '../data/bookData'
const section = getSectionById(id)
After:
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:
import { getAllSections } from '../data/bookData'
const results = getAllSections().filter(s => s.title.includes(keyword))
After:
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
// 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 请求失败
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)
}
}
网络超时
// 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. 懒加载
// 只在需要时加载章节内容
useEffect(() => {
if (visible) {
loadContent()
}
}, [visible])
3. 预加载
// 预加载下一章内容
useEffect(() => {
if (content && nextChapterId) {
// 延迟 2 秒预加载
const timer = setTimeout(() => {
getChapterById(nextChapterId)
}, 2000)
return () => clearTimeout(timer)
}
}, [content, nextChapterId])
🧪 测试清单
API 集成测试
- 章节列表加载成功
- 章节详情加载成功
- 用户信息获取成功
- 配置加载成功
- 搜索功能正常
- 错误处理正确
缓存测试
- 首次加载从 API 获取
- 第二次加载从缓存读取
- 缓存过期后重新加载
- 缓存数据格式正确
跨平台测试
- Web 环境正常
- 小程序环境正常
- 数据格式一致
📚 相关文档
总结:API 集成层已完成,接下来需要更新各个页面组件,将静态数据改为从 API 加载。