Files
soul-yongping/开发文档/8、部署/API接入说明.md
2026-02-04 10:35:50 +08:00

13 KiB
Raw Blame History

小程序 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

功能

  1. 从 API 加载章节列表
  2. 缓存到本地30分钟
  3. 转换数据格式API → bookData
  4. 提供辅助函数

使用示例

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. 支持重新加载

使用示例

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
  • WeblocalStorage

缓存时长

  • 章节列表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 环境正常
  • 小程序环境正常
  • 数据格式一致

📚 相关文档

  1. API 集成层代码
  2. 章节列表 Hook
  3. 章节内容 Hook

总结API 集成层已完成,接下来需要更新各个页面组件,将静态数据改为从 API 加载。