- 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
122 lines
4.1 KiB
Go
122 lines
4.1 KiB
Go
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_ui(JSON 对象)与默认文案;占位符 {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
|
||
}
|