From dc3597c906e84f9da4f8c59ccf916bcbd0c7aedb Mon Sep 17 00:00:00 2001 From: Alex-larget <33240357+Alex-larget@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:20:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=8D=95=E9=A1=B5=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E7=9A=84=E7=94=A8=E6=88=B7=E5=BC=95=E5=AF=BC=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7=E5=9C=A8?= =?UTF-8?q?=E6=9C=8B=E5=8F=8B=E5=9C=88=E7=AD=89=E7=8E=AF=E5=A2=83=E4=B8=AD?= =?UTF-8?q?=E8=83=BD=E5=A4=9F=E9=A1=BA=E5=88=A9=E7=99=BB=E5=BD=95=E5=92=8C?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E5=AE=8C=E6=95=B4=E5=86=85=E5=AE=B9=E3=80=82?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=AB=A0=E8=8A=82=E5=86=85=E5=AE=B9=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=9C=AA?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E7=94=A8=E6=88=B7=E6=97=A0=E6=B3=95=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E5=AE=8C=E6=95=B4=E5=86=85=E5=AE=B9=E3=80=82=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=89=8B=E6=9C=BA=E5=8F=B7=E5=90=8C=E6=AD=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E8=B5=84?= =?UTF-8?q?=E6=96=99=E7=AE=A1=E7=90=86=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/app.js | 59 ++++++++++++++++++++++++- miniprogram/pages/index/index.js | 27 +++++++++++ miniprogram/pages/index/index.wxml | 3 +- miniprogram/pages/index/index.wxss | 6 ++- miniprogram/pages/my/my.js | 16 +++++++ miniprogram/pages/read/read.js | 49 +++++++++++++++----- miniprogram/project.private.config.json | 18 +++++++- soul-api/internal/handler/book.go | 6 ++- 8 files changed, 165 insertions(+), 19 deletions(-) diff --git a/miniprogram/app.js b/miniprogram/app.js index 58b88341..9eb4fdec 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -8,8 +8,8 @@ const { parseScene } = require('./utils/scene.js') App({ globalData: { // API基础地址 - 连接真实后端 - // baseUrl: 'https://soulapi.quwanzhi.com', - baseUrl: 'https://souldev.quwanzhi.com', + baseUrl: 'https://soulapi.quwanzhi.com', + // baseUrl: 'https://souldev.quwanzhi.com', // baseUrl: 'http://localhost:8080', @@ -61,6 +61,10 @@ App({ // TabBar相关 currentTab: 0, + // 是否处于「单页模式」(如朋友圈文章里的单页预览) + // 用于在受限环境下给出引导文案,提示用户点击底部「前往小程序」进入完整体验 + isSinglePageMode: false, + // 更新检测:上次检测时间戳,避免频繁请求 lastUpdateCheck: 0 }, @@ -69,6 +73,16 @@ App({ this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || [] // 获取系统信息 this.getSystemInfo() + + // 场景值兜底:1154 为「朋友圈单页模式」进入 + try { + const launchOpts = wx.getLaunchOptionsSync ? wx.getLaunchOptionsSync() : null + if (launchOpts && launchOpts.scene === 1154) { + this.globalData.isSinglePageMode = true + } + } catch (e) { + console.warn('[App] 读取 LaunchOptions 失败:', e) + } // 检查登录状态 this.checkLoginStatus() @@ -208,6 +222,11 @@ App({ const systemInfo = wx.getSystemInfoSync() this.globalData.systemInfo = systemInfo this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44 + + // 微信在单页模式下会在 systemInfo.mode 标记 singlePage + if (systemInfo.mode === 'singlePage') { + this.globalData.isSinglePageMode = true + } // 计算导航栏高度 const menuButton = wx.getMenuButtonBoundingClientRect() @@ -219,6 +238,33 @@ App({ } }, + /** + * 若当前处于朋友圈等「单页模式」,在尝试登录/购买前给用户友好提示, + * 引导用户点击底部「前往小程序」进入完整小程序再操作。 + * 返回 false 表示应中断当前操作。 + */ + ensureFullAppForAuth() { + // 每次调用时再做一次兜底检测,避免全局标记遗漏 + try { + const sys = wx.getSystemInfoSync() + if (sys && sys.mode === 'singlePage') { + this.globalData.isSinglePageMode = true + } + } catch (e) { + console.warn('[App] ensureFullAppForAuth getSystemInfoSync error:', e) + } + + if (!this.globalData.isSinglePageMode) return true + + wx.showModal({ + title: '请前往完整小程序', + content: '当前为朋友圈单页,仅支持部分浏览。如需登录和解锁内容,请点击底部「前往小程序」后再操作。', + showCancel: false, + confirmText: '我知道了', + }) + return false + }, + // 检查登录状态 checkLoginStatus() { try { @@ -386,6 +432,9 @@ App({ // 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”) async login() { + if (!this.ensureFullAppForAuth()) { + return null + } try { const loginRes = await new Promise((resolve, reject) => { wx.login({ success: resolve, fail: reject }) @@ -446,6 +495,9 @@ App({ // 获取openId (支付必需) async getOpenId() { + if (!this.ensureFullAppForAuth()) { + return null + } // 先检查缓存 const cachedOpenId = wx.getStorageSync('openId') if (cachedOpenId) { @@ -499,6 +551,9 @@ App({ // 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode async loginWithPhone(phoneCode) { + if (!this.ensureFullAppForAuth()) { + return null + } try { const loginRes = await new Promise((resolve, reject) => { wx.login({ success: resolve, fail: reject }) diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index a5719e34..0752c1e0 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -370,6 +370,9 @@ Page({ this.setData({ showLeadModal: false, leadPhone: '' }) }, + // 阻止弹窗内部点击事件冒泡到遮罩层 + stopPropagation() {}, + onLeadPhoneInput(e) { this.setData({ leadPhone: (e.detail.value || '').trim() }) }, @@ -407,6 +410,30 @@ Page({ this.setData({ showLeadModal: false, leadPhone: '' }) if (res && res.success) { wx.setStorageSync('lead_last_submit_ts', Date.now()) + + // 若用户资料中尚未保存手机号,则顺手同步到资料(不影响本次提交结果) + try { + const currentPhone = (app.globalData.userInfo?.phone || '').trim() + if (!currentPhone && userId) { + await app.request({ + url: '/api/miniprogram/user/profile', + method: 'POST', + data: { + userId, + phone + } + }) + if (app.globalData.userInfo) { + app.globalData.userInfo.phone = phone + wx.setStorageSync('userInfo', app.globalData.userInfo) + } + wx.setStorageSync('user_phone', phone) + } + } catch (e) { + // 资料同步失败不影响前端提示 + console.log('[Index] 同步手机号到用户资料失败:', e && e.message) + } + wx.showToast({ title: res.message || '提交成功,卡若会尽快联系您', icon: 'success' }) } else { wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' }) diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index 4506b61a..8405eb04 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -189,7 +189,8 @@ - + + 留下联系方式 方便卡若与您联系 diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss index bb2435de..77f8d98b 100644 --- a/miniprogram/pages/index/index.wxss +++ b/miniprogram/pages/index/index.wxss @@ -898,7 +898,11 @@ .lead-btn { flex: 1; height: 80rpx; - line-height: 80rpx; + /* 使用 flex 垂直居中文本,避免小程序默认 padding 导致按钮文字下沉 */ + display: flex; + align-items: center; + justify-content: center; + padding: 0; border-radius: 16rpx; font-size: 30rpx; font-weight: 500; diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index 33735f30..213b700e 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -619,6 +619,22 @@ Page({ // 显示登录弹窗(每次打开时协议未勾选,符合审核要求) showLogin() { + // 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」 + try { + const sys = wx.getSystemInfoSync() + const isSinglePage = (sys && sys.mode === 'singlePage') || getApp().globalData.isSinglePageMode + if (isSinglePage) { + wx.showModal({ + title: '请前往完整小程序', + content: '当前为朋友圈单页,仅支持部分体验。想登录并管理账户,请点击底部「前往小程序」后再操作。', + showCancel: false, + confirmText: '我知道了', + }) + return + } + } catch (e) { + console.warn('[My] 检测单页模式失败,回退为正常登录弹窗:', e) + } try { this.setData({ showLoginModal: true, agreeProtocol: false }) } catch (e) { diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index 8232d308..d784641b 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -106,7 +106,9 @@ Page({ id = ch.id } else { try { - const chRes = await app.request({ url: `/api/miniprogram/book/chapter/by-mid/${mid}`, silent: true }) + const resolveUrl = `/api/miniprogram/book/chapter/by-mid/${mid}` + const uid = app.globalData.userInfo?.id + const chRes = await app.request({ url: uid ? resolveUrl + '?userId=' + encodeURIComponent(uid) : resolveUrl, silent: true }) if (chRes && chRes.id) id = chRes.id } catch (e) { console.warn('[Read] by-mid 解析失败:', e) @@ -223,11 +225,13 @@ Page({ } this.setData({ section }) - if (res && res.content) { - const { lines, segments } = contentParser.parseContent(res.content) + // 已解锁用 data.content(完整内容),未解锁用 content(预览);先 determineAccessState 再 loadContent 保证顺序正确 + const displayContent = accessManager.canAccessFullContent(accessState) ? (res.data?.content ?? res.content) : res.content + if (res && displayContent) { + const { lines, segments } = contentParser.parseContent(displayContent) const previewCount = Math.ceil(lines.length * 0.2) const updates = { - content: res.content, + content: displayContent, contentParagraphs: lines, contentSegments: segments, previewParagraphs: lines.slice(0, previewCount), @@ -236,8 +240,8 @@ Page({ } if (res.mid) updates.sectionMid = res.mid this.setData(updates) - // 写入本地缓存,供离线/重试降级使用 - try { wx.setStorageSync(cacheKey, res) } catch (_) {} + // 写入本地缓存(存 displayContent,供离线/重试降级使用) + try { wx.setStorageSync(cacheKey, { ...res, content: displayContent }) } catch (_) {} if (accessManager.canAccessFullContent(accessState)) { app.markSectionAsRead(id) } @@ -314,15 +318,20 @@ Page({ return titles[id] || `章节 ${id}` }, - // 根据 id/mid 构造章节接口路径(优先使用 mid) + // 根据 id/mid 构造章节接口路径(优先使用 mid)。必须带 userId 才能让后端正确判断付费用户并返回完整内容 _getChapterUrl(params = {}) { const { id, mid } = params const finalMid = (mid !== undefined && mid !== null) ? mid : this.data.sectionMid + let url if (finalMid) { - return `/api/miniprogram/book/chapter/by-mid/${finalMid}` + url = `/api/miniprogram/book/chapter/by-mid/${finalMid}` + } else { + const finalId = id || this.data.sectionId + url = `/api/miniprogram/book/chapter/${finalId}` } - const finalId = id || this.data.sectionId - return `/api/miniprogram/book/chapter/${finalId}` + const userId = app.globalData.userInfo?.id + if (userId) url += (url.includes('?') ? '&' : '?') + 'userId=' + encodeURIComponent(userId) + return url }, @@ -651,8 +660,8 @@ Page({ : '📚 Soul创业派对 - 真实商业故事' return { title: shareTitle, - path: ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`, - imageUrl: '/assets/share-cover.png' + path: ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}` + // 不设置 imageUrl,使用当前阅读页截图作为分享卡片中间图片 } }, @@ -670,6 +679,22 @@ Page({ // 显示登录弹窗(每次打开协议未勾选,符合审核要求) showLoginModal() { + // 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」 + try { + const sys = wx.getSystemInfoSync() + const isSinglePage = (sys && sys.mode === 'singlePage') || app.globalData.isSinglePageMode + if (isSinglePage) { + wx.showModal({ + title: '请前往完整小程序', + content: '当前为朋友圈单页,仅支持部分浏览。想登录继续阅读,请点击底部「前往小程序」后再操作。', + showCancel: false, + confirmText: '我知道了', + }) + return + } + } catch (e) { + console.warn('[Read] 检测单页模式失败,回退为正常登录流程:', e) + } try { this.setData({ showLoginModal: true, agreeProtocol: false }) } catch (e) { diff --git a/miniprogram/project.private.config.json b/miniprogram/project.private.config.json index 12b10c37..964ea715 100644 --- a/miniprogram/project.private.config.json +++ b/miniprogram/project.private.config.json @@ -23,12 +23,26 @@ "condition": { "miniprogram": { "list": [ + { + "name": "pages/my/my", + "pathName": "pages/my/my", + "query": "", + "scene": null, + "launchMode": "singlePage" + }, + { + "name": "pages/read/read", + "pathName": "pages/read/read", + "query": "mid=20", + "launchMode": "default", + "scene": null + }, { "name": "pages/read/read", "pathName": "pages/read/read", "query": "mid=1", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null } ] } diff --git a/soul-api/internal/handler/book.go b/soul-api/internal/handler/book.go index 6865262f..9c06b295 100644 --- a/soul-api/internal/handler/book.go +++ b/soul-api/internal/handler/book.go @@ -199,9 +199,13 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) { returnContent = previewContent(ch.Content) } + // data 中的 content 必须与外层 content 一致,避免泄露完整内容给未授权用户 + chForResponse := ch + chForResponse.Content = returnContent + out := gin.H{ "success": true, - "data": ch, + "data": chForResponse, "content": returnContent, "chapterTitle": ch.ChapterTitle, "partTitle": ch.PartTitle,