feat: 支持章节通过 mid 进行访问,优化阅读跳转逻辑。新增章节数据结构,包含章节的 mid 信息,提升用户体验。更新 API 以支持通过 mid 查询章节内容,确保兼容性与灵活性。
This commit is contained in:
@@ -73,25 +73,29 @@ Page({
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
// 扫码进入时:options.id 无;id 可能在 app.globalData.initialSectionId(App 解析 query.scene)
|
||||
// 或直接在 options.scene 中(页面 query 为 ?scene=id%3D1.1,后端把 & 转成 _)
|
||||
// 支持 mid(优先)或 id:mid 用于新链接,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] 未获取到章节 id,options:', options)
|
||||
if (!mid && !id) {
|
||||
console.warn('[Read] 未获取到章节 mid/id,options:', 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)
|
||||
// 先拉取章节获取 id(mid 时必需;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) {
|
||||
|
||||
Reference in New Issue
Block a user