重构推荐码处理逻辑,整合 scene 解析功能,优化扫码参数接收。更新阅读页面逻辑,支持通过 scene 解析 mid、id 和 ref,提升用户体验。新增二维码生成逻辑,确保与 scene 生成闭环。
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
199
miniprogram/pages/read/FLOW.txt
Normal file
199
miniprogram/pages/read/FLOW.txt
Normal file
@@ -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 内 & 或 _ 分隔)
|
||||
|
||||
================================================================================
|
||||
@@ -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) => {
|
||||
|
||||
45
miniprogram/utils/scene.js
Normal file
45
miniprogram/utils/scene.js
Normal file
@@ -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 }
|
||||
Reference in New Issue
Block a user