diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js
index b3b7899a..677f8bec 100644
--- a/miniprogram/pages/read/read.js
+++ b/miniprogram/pages/read/read.js
@@ -28,6 +28,7 @@ const mpPagePopups = require('../../utils/mpPagePopups.js')
const READ_UI_DEFAULTS = {
singlePageUnlockTitle: '解锁完整内容',
singlePagePayButtonText: '支付 ¥{price} 解锁全文',
+ /** 单页展开区首行:付款引导;其下为分隔线 + shareTipLine + fullPaywallTip */
singlePageExpandedHint: '预览页不能直接付款,务必先点底栏「前往小程序」。',
payTapModalTitle: '解锁说明',
payTapModalContent:
@@ -613,7 +614,8 @@ Page({
const parseCfg = getContentParseConfig()
const sectionPrice = this.data.sectionPrice ?? 1
let res = prefetchedChapter
- if (!res || !res.content) {
+ const hasChapterBody = (r) => r && String(r.data?.content ?? r.content ?? '').length > 0
+ if (!res || !hasChapterBody(res)) {
res = await app.request({ url: this._getChapterUrl({ id }), silent: true })
}
this._applyChapterMetaFromResponse(res)
@@ -656,14 +658,15 @@ Page({
console.error('[Read] 加载内容失败,尝试本地缓存:', e)
try {
const cached = wx.getStorageSync(cacheKey)
- if (cached && cached.content) {
+ const cachedDisplay = cached && (cached.data?.content ?? cached.content)
+ if (cached && String(cachedDisplay || '').length) {
await app.getReadExtras()
this._applyChapterMetaFromResponse(cached)
- const { lines, segments } = contentParser.parseContent(cached.content, getContentParseConfig())
+ const { lines, segments } = contentParser.parseContent(cachedDisplay, getContentParseConfig())
// 预览比例由前端按高度裁切,这里渲染完整内容
const previewCount = lines.length
this.setData({
- content: cached.content,
+ content: cachedDisplay,
contentParagraphs: lines,
contentSegments: normalizeMentionSegments(segments),
previewParagraphs: lines.slice(0, previewCount),
diff --git a/miniprogram/pages/read/read.wxml b/miniprogram/pages/read/read.wxml
index 6899cced..eadcf8bd 100644
--- a/miniprogram/pages/read/read.wxml
+++ b/miniprogram/pages/read/read.wxml
@@ -149,7 +149,12 @@
审核中,暂不支持购买
- {{readUi.singlePageExpandedHint || '预览不可付款,请点底部「前往小程序」。'}}
+
+ {{readUi.singlePageExpandedHint || '预览页不能直接付款,务必先点底栏「前往小程序」。'}}
+
+ {{readUi.shareTipLine || '好友经你分享购买,你可获得约 90% 收益'}}
+ {{readUi.fullPaywallTip || '转发给需要的人,一起学习还能赚佣金'}}
+
@@ -236,7 +241,12 @@
审核中,暂不支持购买
- {{readUi.singlePageExpandedHint || '预览不可付款,请点底部「前往小程序」。'}}
+
+ {{readUi.singlePageExpandedHint || '预览页不能直接付款,务必先点底栏「前往小程序」。'}}
+
+ {{readUi.shareTipLine || '好友经你分享购买,你可获得约 90% 收益'}}
+ {{readUi.fullPaywallTip || '转发给需要的人,一起学习还能赚佣金'}}
+
diff --git a/miniprogram/pages/read/read.wxss b/miniprogram/pages/read/read.wxss
index 4d6543a7..fafe0157 100644
--- a/miniprogram/pages/read/read.wxss
+++ b/miniprogram/pages/read/read.wxss
@@ -299,6 +299,13 @@
}
.paywall--single-preview .paywall-title {
margin-bottom: 20rpx;
+ font-size: 34rpx;
+}
+.paywall--single-preview .paywall-desc {
+ font-size: 24rpx;
+}
+.paywall--single-preview .purchase-section .btn-label {
+ font-size: 26rpx;
}
/* 朋友圈单页图二:首段说明 + 与购买行间距收紧 */
.paywall-sp-lead {
@@ -328,12 +335,35 @@
text-align: center;
color: rgba(255, 255, 255, 0.58);
}
-.paywall-desc--moments-expanded {
- margin-top: 28rpx !important;
- margin-bottom: 0 !important;
- font-size: 26rpx !important;
- line-height: 1.45;
+/* 单页:点「解锁说明」后展开 — 首行引导 / 分隔线 / 两行分润说明(与运营稿一致) */
+.paywall-expanded-stack {
+ margin-top: 28rpx;
padding: 0 8rpx;
+ width: 100%;
+ box-sizing: border-box;
+}
+.paywall-expanded-lead {
+ display: block;
+ font-size: 22rpx;
+ line-height: 1.45;
+ color: rgba(255, 255, 255, 0.55);
+ text-align: center;
+}
+.paywall-expanded-divider {
+ height: 0;
+ border-top: 1rpx solid rgba(255, 255, 255, 0.08);
+ margin: 20rpx 0;
+}
+.paywall-expanded-line {
+ display: block;
+ font-size: 22rpx;
+ line-height: 1.45;
+ color: rgba(255, 255, 255, 0.5);
+ text-align: center;
+ margin-bottom: 12rpx;
+}
+.paywall-expanded-line:last-child {
+ margin-bottom: 0;
}
/* 朋友圈单页:未点解锁前的一行轻提示 */
diff --git a/miniprogram/project.private.config.json b/miniprogram/project.private.config.json
index 7d4bdd14..0133be56 100644
--- a/miniprogram/project.private.config.json
+++ b/miniprogram/project.private.config.json
@@ -28,7 +28,7 @@
"pathName": "pages/read/read",
"query": "mid=233",
"scene": null,
- "launchMode": "default"
+ "launchMode": "singlePage"
},
{
"name": "个人资料",
diff --git a/soul-api/internal/handler/book.go b/soul-api/internal/handler/book.go
index 339ac422..a6419913 100644
--- a/soul-api/internal/handler/book.go
+++ b/soul-api/internal/handler/book.go
@@ -644,7 +644,8 @@ func getUnpaidPreviewPercent(db *gorm.DB) int {
// findChapterAndRespond 按条件查章节并返回统一格式
// 免费判断优先级:system_config.free_chapters / chapter_config.freeChapters > chapters.is_free/price
-// 付费章节:若请求携带 userId 且有购买权限则返回完整 content,否则返回 previewContent
+// data.content 始终返回完整 HTML;未解锁时的「试读比例」由小程序对渲染后高度做 overflow 裁切(见 miniprogram read _updatePreviewClipHeight)。
+// 纯文本截取 previewContent 仅用于 H5 落地页等,不再用于小程序读章节响应。
// content 缓存:优先 Redis,命中时仅查元数据(不含 LONGTEXT),未命中时查全量并回填缓存
func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
var ch model.Chapter
@@ -679,27 +680,18 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
isFree = true
}
- // 确定返回的 content:免费直接返回,付费须校验购买权限
userID := c.Query("userId")
isPremium := ch.EditionPremium != nil && *ch.EditionPremium
hasFullAccess := isFree || checkUserChapterAccess(db, userID, ch.ID, isPremium)
- var returnContent string
- // 未解锁:正文截取见 chapter_preview.go(HTML 剥标签后与 H5 一致)
- if hasFullAccess {
- returnContent = ch.Content
- } else {
- percent := chapterPreviewPercent(db, &ch)
- returnContent = previewContent(ch.Content, percent)
- }
+ effectivePct := chapterPreviewPercent(db, &ch)
+ returnContent := ch.Content
- // data 中的 content 必须与外层 content 一致,避免泄露完整内容给未授权用户
chForResponse := ch
chForResponse.Content = returnContent
chForResponse.PartTitle = sanitizeChapterTitleField(ch.PartTitle)
chForResponse.ChapterTitle = sanitizeChapterTitleField(ch.ChapterTitle)
chForResponse.SectionTitle = sanitizeChapterTitleField(ch.SectionTitle)
- effectivePct := chapterPreviewPercent(db, &ch)
out := gin.H{
"success": true,
"data": chForResponse,
@@ -715,7 +707,7 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
"readPreviewUi": mergeReadPreviewUI(db),
}
if !hasFullAccess {
- out["previewPercent"] = getUnpaidPreviewPercent(db)
+ out["previewPercent"] = effectivePct
}
// 文章详情内直接输出上一篇/下一篇,省去单独请求
if list := getOrderedChapterList(); len(list) > 0 {
diff --git a/soul-api/internal/handler/chapter_preview.go b/soul-api/internal/handler/chapter_preview.go
index 5999c2dd..4ba4f108 100644
--- a/soul-api/internal/handler/chapter_preview.go
+++ b/soul-api/internal/handler/chapter_preview.go
@@ -44,7 +44,7 @@ func chapterPreviewPercent(db *gorm.DB, ch *model.Chapter) int {
return g
}
-// previewContent 取内容的前 percent%(按纯文本 rune 计;原 HTML 先剥标签),与 h5 落地页一致
+// previewContent 取内容的前 percent%(按纯文本 rune 计;原 HTML 先剥标签)——用于 H5 落地页等;小程序读章节接口应返回全文并由端上按渲染高度裁切。
func previewContent(content string, percent int) string {
work := content
if strings.Contains(content, "<") && strings.Contains(content, ">") {