diff --git a/miniprogram/app.js b/miniprogram/app.js index fa272036..e02ba9fc 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -3,6 +3,8 @@ * 开发: 卡若 */ +const { parseScene } = require('./utils/scene.js') + App({ globalData: { // API基础地址 - 连接真实后端 @@ -83,29 +85,17 @@ App({ this.handleReferralCode(options) }, - // 处理推荐码绑定(支持 query.ref 与扫码 scene;scene 可能为 ref=SOULXXX 或 id=1.1_ref=xxx,后端会把 & 转成 _) - // 扫码进入时:options.scene 为场景值(1047),自定义 scene 在 options.query.scene + // 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环 handleReferralCode(options) { const query = options?.query || {} let refCode = query.ref || query.referralCode - // 扫码进入时自定义 scene 在 query.scene;options.scene 为场景值(1047) - const sceneStr = query?.scene || (typeof options?.scene === 'string' ? options.scene : null) + const sceneStr = (options && (typeof options.scene === 'string' ? options.scene : '')) || '' if (sceneStr) { - const scene = (typeof sceneStr === 'string' ? decodeURIComponent(sceneStr) : '').trim() - // 支持 _ 或 & 作为参数分隔符(后端生成小程序码时会把 & 转为 _) - const parts = scene.split(/[&_]/) - for (const part of parts) { - const eq = part.indexOf('=') - if (eq > 0) { - const key = part.slice(0, eq) - const val = part.slice(eq + 1) - if (key === 'ref') refCode = val - if (key === 'id' && val) this.globalData.initialSectionId = val - if (key === 'mid' && val) this.globalData.initialSectionMid = parseInt(val, 10) || 0 - } - } + const parsed = parseScene(sceneStr) + if (parsed.mid) this.globalData.initialSectionMid = parsed.mid + if (parsed.id) this.globalData.initialSectionId = parsed.id + if (parsed.ref) refCode = parsed.ref } - if (refCode) { console.log('[App] 检测到推荐码:', refCode) diff --git a/miniprogram/pages/read/FLOW.txt b/miniprogram/pages/read/FLOW.txt new file mode 100644 index 00000000..74506f49 --- /dev/null +++ b/miniprogram/pages/read/FLOW.txt @@ -0,0 +1,199 @@ +================================================================================ + miniprogram/pages/read 阅读页 - 当前处理流程 (ASCII) +================================================================================ + +---------- 1. 进入页面 onLoad(options) 总览 ---------- + + +------------------+ + | onLoad(options) | + +--------+---------+ + | + v + +----------------------------------------+ + | sceneStr = options.scene || '' | + | parsed = parseScene(sceneStr) | <-- utils/scene.js + | mid = options.mid || parsed.mid || | + | app.globalData.initialSectionMid| + | id = options.id || parsed.id || | + | app.globalData.initialSectionId | + | ref = options.ref || parsed.ref | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | 清除 app.globalData.initialSection* | + +--------+-------------------------------+ + | + v + [ mid || id ? ] + | + No ---+---> 提示「章节参数缺失」, return + | + Yes v + +----------------------------------------+ + | setData: sectionMid, loading=true | + | if (ref): 存 referral_code, | + | app.handleReferralCode(ref) | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | Promise.all: | + | - accessManager.fetchLatestConfig() | + | - /api/.../purchase-status (若登录) | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | 计算价格/推荐折扣, setData 配置 | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | resolvedId = id | + | if (mid && !id): | + | GET /book/chapter/by-mid/:mid | + | resolvedId = chRes.id | + | prefetchedChapter = chRes | + | setData(sectionId: resolvedId) | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | accessManager.determineAccessState() | + | setData: accessState, canAccess, | + | showPaywall, isLoggedIn | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | await loadContent(mid, resolvedId, | + | accessState, | + | prefetchedChapter) | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | if (canAccess): readingTracker.init() | + | loadNavigation(resolvedId) | + | setData(loading: false) | + +----------------------------------------+ + + +---------- 2. scene 闭环(海报生成 <-> 扫码解析)---------- + + [ 生成海报 ] + | + v + +----------------------------------------+ + | buildScene({ mid?, id?, ref? }) | utils/scene.js + | 例: mid=1, ref=ogpTW5fmXR | + | => "mid=1_ref=ogpTW5fmXR" | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | GET /api/miniprogram/qrcode/image | + | ?scene=mid%3D1_ref%3Dxxx | + | &page=pages/read/read&width=280 | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | 后端: & -> _ (若需要), 调微信生成图 | + | 返回 image/png | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | wx.downloadFile(url) -> tempFilePath | + | canvas drawImage(tempFilePath) 画海报 | + +----------------------------------------+ + + ---------- 用户扫码 ---------- + + +----------------------------------------+ + | 微信: 打开 pages/read/read | + | options.scene = "mid=1_ref=xxx" | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | App.onLaunch(options) | + | handleReferralCode(options) | + | parseScene(options.scene) | + | -> initialSectionMid, initialSectionId| + | -> ref 存 referral_code / 绑定 | + +----------------------------------------+ + | + v + +----------------------------------------+ + | Read.onLoad(options) | + | parseScene(options.scene) -> mid,id,ref| + | 与 app.globalData.initial* 合并 | + | -> 用 mid/id 拉章节, ref 已存 | + +----------------------------------------+ + + +---------- 3. loadContent(mid, id, accessState, prefetched) ---------- + + +------------------+ + | loadContent(...) | + +--------+---------+ + | + v + +----------------------------------------+ + | section = getSectionInfo(id) | + | setData({ section }) | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | prefetched 有内容? | + +--------+-------------------------------+ + | Yes + +-----> res = prefetched (不请求) + | + | No + v + +----------------------------------------+ + | mid ? GET /book/chapter/by-mid/:mid | + | : GET /book/chapter/:id | + +--------+-------------------------------+ + | + v + +----------------------------------------+ + | res && res.content ? | + +--------+-------------------------------+ + | Yes + +-----> setData(content, paragraphs, partTitle, chapterTitle) + | if (canAccess): markSectionAsRead(id) + | setTimeout(drawShareCard, 600) + | + | No / 请求异常 + v + +----------------------------------------+ + | 尝试 wx.getStorageSync(cacheKey) | + | 有缓存 -> setData 内容; 无 -> throw | + +----------------------------------------+ + + +---------- 4. 参数来源优先级(onLoad 内 mid / id / ref)---------- + + mid = options.mid + ?? parseInt(parsed.mid) + ?? app.globalData.initialSectionMid + ?? 0 + + id = options.id + ?? parsed.id + ?? app.globalData.initialSectionId + ?? '' + + ref = options.ref + ?? parsed.ref + ?? '' + + (解析 parsed = parseScene(options.scene),支持 scene 内 & 或 _ 分隔) + +================================================================================ diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index d12f3683..f0cedc92 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -12,6 +12,7 @@ import accessManager from '../../utils/chapterAccessManager' import readingTracker from '../../utils/readingTracker' +const { buildScene, parseScene } = require('../../utils/scene.js') const app = getApp() @@ -79,40 +80,17 @@ Page({ }, async onLoad(options) { - // 支持 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 ((!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) { - 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 - } - } - } + // 官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与海报生成 buildScene 闭环 + const sceneStr = (options && options.scene) || '' + const parsed = parseScene(sceneStr) + const mid = options.mid ? parseInt(options.mid, 10) : (parsed.mid || app.globalData.initialSectionMid || 0) + const id = options.id || parsed.id || app.globalData.initialSectionId + const ref = options.ref || parsed.ref if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid if (app.globalData.initialSectionId) delete app.globalData.initialSectionId - // ref:支持 query.ref 或 scene 中的 ref=xxx(扫码进入时 ref 在 scene 里) - let ref = options.ref - if (!ref && options.scene) { - const sceneStr = (typeof options.scene === 'string' ? decodeURIComponent(options.scene) : '').trim() - const parts = sceneStr.split(/[&_]/) - for (const part of parts) { - const eq = part.indexOf('=') - if (eq > 0) { - const k = part.slice(0, eq) - const v = part.slice(eq + 1) - if (k === 'ref' && v) { ref = v; break } - } - } - } + console.log('[Read] onLoad:', { options, sceneRaw: sceneStr || undefined, parsed, mid, id, ref }) + console.log('[Read] onLoad options:', options) if (!mid && !id) { console.warn('[Read] 未获取到章节 mid/id,options:', options) @@ -241,6 +219,7 @@ Page({ // 【重构】加载章节内容。mid 优先用 by-mid 接口,id 用旧接口;prefetched 避免重复请求 async loadContent(mid, id, accessState, prefetched) { + console.log('[Read] loadContent 请求参数:', { mid, id, accessState: accessState, prefetched: !!prefetched }) try { const section = this.getSectionInfo(id) const sectionPrice = this.data.sectionPrice ?? 1 @@ -1090,12 +1069,15 @@ Page({ const userId = userInfo?.id || '' const safeParagraphs = contentParagraphs || [] - // 通过 GET 接口下载二维码图片,得到 tempFilePath 便于开发工具与真机统一用 drawImage 绘制 + // 与 utils/scene 闭环:生成 scene 用 buildScene,扫码后用 parseScene 解析 let qrcodeTempPath = null try { - const scene = sectionMid - ? (userId ? `mid=${sectionMid}&ref=${userId.slice(0, 10)}` : `mid=${sectionMid}`) - : (userId ? `id=${sectionId}&ref=${userId.slice(0, 10)}` : `id=${sectionId}`) + const refVal = userId ? String(userId).slice(0, 12) : '' + const scene = buildScene({ + mid: sectionMid || undefined, + id: sectionMid ? undefined : (sectionId || ''), + ref: refVal || undefined + }) 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) => { diff --git a/miniprogram/utils/scene.js b/miniprogram/utils/scene.js new file mode 100644 index 00000000..a36808e7 --- /dev/null +++ b/miniprogram/utils/scene.js @@ -0,0 +1,45 @@ +/** + * 小程序码 scene 参数统一编解码(海报生成 ↔ 扫码解析闭环) + * 官方以 options.scene 接收扫码参数;后端生成码时会把 & 转为 _,故解析时同时支持 & 和 _ + * scene 同时可带两个参数:章节标识(mid/id) + 推荐人(ref) + */ + +const SEP = '_' // 生成时统一用 _,与微信实际存储一致,且不占 32 字符限制 + +/** + * 编码:生成海报/分享时组 scene 字符串(同时带 mid或id + ref) + * @param {{ mid?: number, id?: string, ref?: string }} opts + * @returns {string} 如 "mid=1_ref=ogpTW5fmXR" 或 "id=1.1_ref=xxx" + */ +function buildScene(opts) { + const parts = [] + if (opts.mid != null && opts.mid !== '') parts.push(`mid=${opts.mid}`) + if (opts.id) parts.push(`id=${opts.id}`) + if (opts.ref) parts.push(`ref=${opts.ref}`) + return parts.join(SEP) +} + +/** + * 解码:从 options.scene 解析出 mid、id、ref(支持 & 或 _ 分隔) + * @param {string} sceneStr 原始 scene(可能未 decodeURIComponent) + * @returns {{ mid: number, id: string, ref: string }} + */ +function parseScene(sceneStr) { + const res = { mid: 0, id: '', ref: '' } + if (!sceneStr || typeof sceneStr !== 'string') return res + const decoded = decodeURIComponent(String(sceneStr)).trim() + const parts = decoded.split(/[&_]/) + for (const part of parts) { + const eq = part.indexOf('=') + if (eq > 0) { + const k = part.slice(0, eq) + const v = part.slice(eq + 1) + if (k === 'mid') res.mid = parseInt(v, 10) || 0 + if (k === 'id' && v) res.id = v + if (k === 'ref' && v) res.ref = v + } + } + return res +} + +module.exports = { buildScene, parseScene }