Files
soul-yongping/soul-api/internal/handler/chapter_preview.go
卡若 6aa0d27da1 feat: 阅读页与章节预览 API;管理端内容页;book/h5_read;脚本与文档
- miniprogram: read 页与 member-detail/my;SOP 文档
- soul-api: chapter_preview、book/h5_read 调整;VIP 订单回填 SQL
- soul-admin: ContentPage、dist
- scripts: pull_from_baota;content_upload、gitignore、对话规则

Made-with: Cursor
2026-03-27 17:19:21 +08:00

122 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"encoding/json"
"html"
"regexp"
"strings"
"unicode/utf8"
"soul-api/internal/model"
"gorm.io/gorm"
)
var (
reHTMLScript = regexp.MustCompile(`(?is)<script[^>]*>.*?</script>`)
reHTMLStyle = regexp.MustCompile(`(?is)<style[^>]*>.*?</style>`)
reHTMLBr = regexp.MustCompile(`(?i)<\s*br\s*/?>`)
reHTMLPClose = regexp.MustCompile(`(?i)</\s*p\s*>`)
reHTMLTags = regexp.MustCompile(`<[^>]+>`)
)
// stripHTMLToPlainPreview 将正文 HTML 转为纯文本再截取,避免预览截在 <img 中间导致用户看到半截标签
func stripHTMLToPlainPreview(s string) string {
s = reHTMLScript.ReplaceAllString(s, " ")
s = reHTMLStyle.ReplaceAllString(s, " ")
s = reHTMLBr.ReplaceAllString(s, "\n")
s = reHTMLPClose.ReplaceAllString(s, "\n")
s = reHTMLTags.ReplaceAllString(s, "")
s = html.UnescapeString(s)
s = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(s)
return strings.TrimSpace(s)
}
// chapterPreviewPercent 章节单独 preview_percent 覆盖全局 unpaid_preview_percent
func chapterPreviewPercent(db *gorm.DB, ch *model.Chapter) int {
g := getUnpaidPreviewPercent(db)
if ch != nil && ch.PreviewPercent != nil {
p := *ch.PreviewPercent
if p >= 1 && p <= 100 {
return p
}
}
return g
}
// previewContent 取内容的前 percent%(按纯文本 rune 计;原 HTML 先剥标签),与 h5 落地页一致
func previewContent(content string, percent int) string {
work := content
if strings.Contains(content, "<") && strings.Contains(content, ">") {
work = stripHTMLToPlainPreview(content)
}
total := utf8.RuneCountInString(work)
if total == 0 {
return ""
}
if percent < 1 {
percent = 1
}
if percent > 100 {
percent = 100
}
limit := total * percent / 100
if limit < 100 {
limit = 100
}
const maxPreview = 500
if limit > maxPreview {
limit = maxPreview
}
if limit > total {
limit = total
}
runes := []rune(work)
return string(runes[:limit]) + "\n\n……"
}
func defaultReadPreviewUI() map[string]string {
return map[string]string{
"singlePageUnlockTitle": "解锁完整内容",
"singlePagePayButtonText": "支付 ¥{price} 解锁全文",
"singlePageExpandedHint": "预览页不能直接付款,务必先点底栏「前往小程序」。",
"payTapModalTitle": "解锁说明",
"payTapModalContent": "全文 ¥{price}。预览里无法完成支付:请先点屏幕底部「前往小程序」进入完整版,登录后再付款解锁。",
"fullUnlockTitle": "解锁完整内容",
"fullUnlockDesc": "可先上滑阅读预览;需要全文时,点下方「支付¥{price}」查看说明",
"fullLockedProgressText": "已阅读约 {percent}% ,购买后继续阅读",
"fullPaywallTip": "转发给需要的人,一起学习还能赚佣金",
"notLoginUnlockDesc": "已预览约 {percent}% 内容,登录并支付 ¥{price} 后阅读全文",
"notLoginPaywallTip": "分享给好友一起学习,还能赚取佣金",
"shareTipLine": "好友经你分享购买,你可获得约 90% 收益",
"momentsModalTitle": "分享到朋友圈",
"momentsModalContent": "已复制发圈文案(非分享给好友)。\n\n请点击右上角「···」→「分享到朋友圈」粘贴发布。",
"momentsClipboardFooter": "\n\n—— 以上为正文预览约 {percent}% ,搜「卡若创业派对」小程序阅读全文 ——",
"timelineTitleSuffix": "(预览{percent}%",
}
}
// mergeReadPreviewUI 合并 system_config.read_preview_uiJSON 对象)与默认文案;占位符 {percent}/{price} 由小程序替换
func mergeReadPreviewUI(db *gorm.DB) map[string]string {
out := defaultReadPreviewUI()
var row model.SystemConfig
if err := db.Where("config_key = ?", "read_preview_ui").First(&row).Error; err != nil || len(row.ConfigValue) == 0 {
return out
}
var raw map[string]interface{}
if err := json.Unmarshal(row.ConfigValue, &raw); err != nil {
return out
}
for k, v := range raw {
switch t := v.(type) {
case string:
if strings.TrimSpace(t) != "" {
out[k] = strings.TrimSpace(t)
}
default:
// 兼容数字等被误存:忽略非字符串
}
}
return out
}