feat: 支持章节通过 mid 进行访问,优化阅读跳转逻辑。新增章节数据结构,包含章节的 mid 信息,提升用户体验。更新 API 以支持通过 mid 查询章节内容,确保兼容性与灵活性。

This commit is contained in:
乘风
2026-02-12 15:52:35 +08:00
parent 046e686cda
commit a571583be4
18 changed files with 353 additions and 391 deletions

View File

@@ -73,25 +73,29 @@ Page({
},
async onLoad(options) {
// 扫码进入时options.id 无id 可能在 app.globalData.initialSectionIdApp 解析 query.scene
// 或直接在 options.scene 中(页面 query 为 ?scene=id%3D1.1,后端把 & 转成 _
// 支持 mid优先或 idmid 用于新链接id 兼容旧链接
// 扫码进入时mid/id 可能在 options、app.globalData.initialSectionMid/initialSectionId、或 scene 中
let mid = options.mid ? parseInt(options.mid, 10) : (app.globalData.initialSectionMid || 0)
let id = options.id || app.globalData.initialSectionId
if (!id && options.scene) {
if ((!mid || !id) && options.scene) {
const scene = (typeof options.scene === 'string' ? decodeURIComponent(options.scene) : '').trim()
const parts = scene.split(/[&_]/)
for (const part of parts) {
const eq = part.indexOf('=')
if (eq > 0 && part.slice(0, eq) === 'id') {
id = part.slice(eq + 1)
break
if (eq > 0) {
const k = part.slice(0, eq)
const v = part.slice(eq + 1)
if (k === 'mid') mid = parseInt(v, 10) || 0
if (k === 'id' && v) id = v
}
}
}
if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid
if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
const ref = options.ref
if (!id) {
console.warn('[Read] 未获取到章节 idoptions:', options)
if (!mid && !id) {
console.warn('[Read] 未获取到章节 mid/idoptions:', options)
wx.showToast({ title: '章节参数缺失', icon: 'none' })
this.setData({ accessState: 'error', loading: false })
return
@@ -100,12 +104,12 @@ Page({
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight,
sectionId: id,
sectionId: '', // 加载后填充
sectionMid: mid || null,
loading: true,
accessState: 'unknown'
})
// 处理推荐码绑定(异步不阻塞)
if (ref) {
console.log('[Read] 检测到推荐码:', ref)
wx.setStorageSync('referral_code', ref)
@@ -113,7 +117,6 @@ Page({
}
try {
// 【标准流程】1. 拉取最新配置(免费列表、价格)
const config = await accessManager.fetchLatestConfig()
this.setData({
freeIds: config.freeChapters,
@@ -121,8 +124,19 @@ Page({
fullBookPrice: config.prices?.fullbook ?? 9.9
})
// 【标准流程】2. 确定权限状态
const accessState = await accessManager.determineAccessState(id, config.freeChapters)
// 先拉取章节获取 idmid 时必需id 时可直接用)
let resolvedId = id
let prefetchedChapter = null
if (mid && !id) {
const chRes = await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`)
if (chRes && chRes.id) {
resolvedId = chRes.id
prefetchedChapter = chRes
}
}
this.setData({ sectionId: resolvedId })
const accessState = await accessManager.determineAccessState(resolvedId, config.freeChapters)
const canAccess = accessManager.canAccessFullContent(accessState)
this.setData({
@@ -132,16 +146,13 @@ Page({
showPaywall: !canAccess
})
// 【标准流程】3. 加载内容
await this.loadContent(id, accessState)
await this.loadContent(mid, resolvedId, accessState, prefetchedChapter)
// 【标准流程】4. 如果有权限,初始化阅读追踪
if (canAccess) {
readingTracker.init(id)
readingTracker.init(resolvedId)
}
// 5. 加载导航
this.loadNavigation(id)
this.loadNavigation(resolvedId)
} catch (e) {
console.error('[Read] 初始化失败:', e)
@@ -184,8 +195,8 @@ Page({
})
},
// 【重构】加载章节内容(专注于内容加载,权限判断已在 onLoad 中由 accessManager 完成)
async loadContent(id, accessState) {
// 【重构】加载章节内容。mid 优先用 by-mid 接口id 用旧接口prefetched 避免重复请求
async loadContent(mid, id, accessState, prefetched) {
try {
const section = this.getSectionInfo(id)
const sectionPrice = this.data.sectionPrice ?? 1
@@ -194,25 +205,29 @@ Page({
}
this.setData({ section })
// 从 API 获取内容
const res = await app.request(`/api/miniprogram/book/chapter/${id}`)
let res = prefetched
if (!res) {
res = mid
? await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`)
: await app.request(`/api/miniprogram/book/chapter/${id}`)
}
if (res && res.content) {
const lines = res.content.split('\n').filter(line => line.trim())
const previewCount = Math.ceil(lines.length * 0.2)
this.setData({
const updates = {
content: res.content,
contentParagraphs: lines,
previewParagraphs: lines.slice(0, previewCount),
partTitle: res.partTitle || '',
chapterTitle: res.chapterTitle || ''
})
// 如果有权限,标记为已读
}
if (res.mid) updates.sectionMid = res.mid
this.setData(updates)
if (accessManager.canAccessFullContent(accessState)) {
app.markSectionAsRead(id)
}
setTimeout(() => this.drawShareCard(), 600)
}
} catch (e) {
console.error('[Read] 加载内容失败:', e)
@@ -284,50 +299,6 @@ Page({
}
return titles[id] || `章节 ${id}`
},
// 加载内容 - 三级降级方案API → 本地缓存 → 备用API
async loadContent(id) {
const cacheKey = `chapter_${id}`
// 1. 优先从API获取
try {
const res = await this.fetchChapterWithTimeout(id, 5000)
if (res && res.content) {
this.setData({ section: this.getSectionInfo(id) })
this.setChapterContent(res)
wx.setStorageSync(cacheKey, res)
console.log('[Read] 从API加载成功:', id)
setTimeout(() => this.drawShareCard(), 600)
return
}
} catch (e) {
console.warn('[Read] API加载失败尝试本地缓存:', e.message)
}
// 2. API失败尝试从本地缓存读取
try {
const cached = wx.getStorageSync(cacheKey)
if (cached && cached.content) {
this.setData({ section: this.getSectionInfo(id) })
this.setChapterContent(cached)
console.log('[Read] 从本地缓存加载成功:', id)
this.silentRefresh(id)
setTimeout(() => this.drawShareCard(), 600)
return
}
} catch (e) {
console.warn('[Read] 本地缓存读取失败')
}
// 3. 都失败,显示加载中并持续重试
this.setData({
contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'],
previewParagraphs: ['章节内容加载中...']
})
// 延迟重试最多3次
this.retryLoadContent(id, 3)
},
// 带超时的章节请求
fetchChapterWithTimeout(id, timeout = 5000) {
@@ -405,7 +376,7 @@ Page({
},
// 加载导航
// 加载导航prevSection/nextSection 含 mid 时用于跳转,否则用 id
loadNavigation(id) {
const sectionOrder = [
'preface', '1.1', '1.2', '1.3', '1.4', '1.5',
@@ -421,14 +392,19 @@ Page({
'11.1', '11.2', '11.3', '11.4', '11.5',
'epilogue'
]
const bookData = app.globalData.bookData || []
const idToMid = {}
bookData.forEach(ch => {
if (ch.id && ch.mid) idToMid[ch.id] = ch.mid
})
const currentIndex = sectionOrder.indexOf(id)
const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null
const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null
this.setData({
prevSection: prevId ? { id: prevId, title: this.getSectionTitle(prevId) } : null,
nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null
prevSection: prevId ? { id: prevId, mid: idToMid[prevId], title: this.getSectionTitle(prevId) } : null,
nextSection: nextId ? { id: nextId, mid: idToMid[nextId], title: this.getSectionTitle(nextId) } : null
})
},
@@ -543,14 +519,13 @@ Page({
// 统一分享配置(底部「推荐给好友」与右下角分享按钮均走此配置,由 onShareAppMessage 使用)
getShareConfig() {
const { section, sectionId, shareImagePath } = this.data
const { section, sectionId, sectionMid, shareImagePath } = this.data
const ref = app.getMyReferralCode()
const shareTitle = section?.title
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
: '📚 Soul创业派对 - 真实商业故事'
const path = ref
? `/pages/read/read?id=${sectionId}&ref=${ref}`
: `/pages/read/read?id=${sectionId}`
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
const path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
return {
title: shareTitle,
path,
@@ -563,11 +538,12 @@ Page({
},
onShareTimeline() {
const { section, sectionId } = this.data
const { section, sectionId, sectionMid } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
return {
title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
query: ref ? `id=${sectionId}&ref=${ref}` : `id=${sectionId}`
query: ref ? `${q}&ref=${ref}` : q
}
},
@@ -666,7 +642,7 @@ Page({
// 4. 如果已解锁,重新加载内容并初始化阅读追踪
if (canAccess) {
await this.loadContent(this.data.sectionId, newAccessState)
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
readingTracker.init(this.data.sectionId)
}
@@ -737,6 +713,7 @@ Page({
// 更新本地购买状态
app.globalData.hasFullBook = checkRes.data.hasFullBook
app.globalData.purchasedSections = checkRes.data.purchasedSections || []
app.globalData.sectionMidMap = checkRes.data.sectionMidMap || {}
// 检查是否已购买
if (type === 'section' && sectionId) {
@@ -953,7 +930,7 @@ Page({
})
// 4. 重新加载全文
await this.loadContent(this.data.sectionId, newAccessState)
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
// 5. 初始化阅读追踪
if (canAccess) {
@@ -990,6 +967,7 @@ Page({
// 更新全局购买状态
app.globalData.hasFullBook = res.data.hasFullBook
app.globalData.purchasedSections = res.data.purchasedSections || []
app.globalData.sectionMidMap = res.data.sectionMidMap || {}
app.globalData.matchCount = res.data.matchCount ?? 0
app.globalData.matchQuota = res.data.matchQuota || null
@@ -1027,17 +1005,19 @@ Page({
})
},
// 跳转到上一篇
goToPrev() {
if (this.data.prevSection) {
wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` })
const s = this.data.prevSection
if (s) {
const q = s.mid ? `mid=${s.mid}` : `id=${s.id}`
wx.redirectTo({ url: `/pages/read/read?${q}` })
}
},
// 跳转到下一篇
goToNext() {
if (this.data.nextSection) {
wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` })
const s = this.data.nextSection
if (s) {
const q = s.mid ? `mid=${s.mid}` : `id=${s.id}`
wx.redirectTo({ url: `/pages/read/read?${q}` })
}
},
@@ -1051,7 +1031,7 @@ Page({
wx.showLoading({ title: '生成中...' })
this.setData({ showPosterModal: true, isGeneratingPoster: true })
const { section, contentParagraphs, sectionId } = this.data
const { section, contentParagraphs, sectionId, sectionMid } = this.data
const userInfo = app.globalData.userInfo
const userId = userInfo?.id || ''
const safeParagraphs = contentParagraphs || []
@@ -1059,7 +1039,9 @@ Page({
// 通过 GET 接口下载二维码图片,得到 tempFilePath 便于开发工具与真机统一用 drawImage 绘制
let qrcodeTempPath = null
try {
const scene = userId ? `id=${sectionId}&ref=${userId.slice(0, 10)}` : `id=${sectionId}`
const scene = sectionMid
? (userId ? `mid=${sectionMid}&ref=${userId.slice(0, 10)}` : `mid=${sectionMid}`)
: (userId ? `id=${sectionId}&ref=${userId.slice(0, 10)}` : `id=${sectionId}`)
const baseUrl = app.globalData.baseUrl || ''
const url = `${baseUrl}/api/miniprogram/qrcode/image?scene=${encodeURIComponent(scene)}&page=${encodeURIComponent('pages/read/read')}&width=280`
qrcodeTempPath = await new Promise((resolve) => {
@@ -1272,7 +1254,7 @@ Page({
})
// 重新加载内容
await this.loadContent(this.data.sectionId, newAccessState)
await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null)
// 如果有权限,初始化阅读追踪
if (canAccess) {